@push.rocks/taskbuffer 4.0.0 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/readme.md CHANGED
@@ -1,163 +1,169 @@
1
1
  # @push.rocks/taskbuffer 🚀
2
2
 
3
- > **Modern TypeScript task orchestration with smart buffering, scheduling, and real-time progress tracking**
3
+ > **Modern TypeScript task orchestration with smart buffering, scheduling, labels, and real-time event streaming**
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@push.rocks/taskbuffer.svg)](https://www.npmjs.com/package/@push.rocks/taskbuffer)
6
6
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue.svg)](https://www.typescriptlang.org/)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
8
 
9
+ ## Issue Reporting and Security
10
+
11
+ For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
12
+
9
13
  ## 🌟 Features
10
14
 
11
- - **🎯 Type-Safe Task Management** - Full TypeScript support with generics and type inference
12
- - **📊 Real-Time Progress Tracking** - Step-based progress with percentage weights
13
- - **⚡ Smart Buffering** - Intelligent request debouncing and batching
14
- - **⏰ Cron Scheduling** - Schedule tasks with cron expressions
15
- - **🔄 Task Chains & Parallel Execution** - Sequential and parallel task orchestration
16
- - **🎨 Web Component Dashboard** - Real-time visualization of task execution
17
- - **📈 Comprehensive Metadata** - Track execution history, duration, and status
18
- - **🔒 Thread-Safe Operations** - Concurrency control and execution limits
19
- - **🎭 Event-Driven Architecture** - Observable task lifecycle events
15
+ - **🎯 Type-Safe Task Management** Full TypeScript support with generics and type inference
16
+ - **📊 Real-Time Progress Tracking** Step-based progress with percentage weights
17
+ - **⚡ Smart Buffering** Intelligent request debouncing and batching
18
+ - **⏰ Cron Scheduling** Schedule tasks with cron expressions
19
+ - **🔗 Task Chains & Parallel Execution** Sequential and parallel task orchestration
20
+ - **🏷️ Labels** Attach arbitrary `Record<string, string>` metadata (userId, tenantId, etc.) for multi-tenant filtering
21
+ - **📡 Push-Based Events** rxjs `Subject<ITaskEvent>` on every Task and TaskManager for real-time state change notifications
22
+ - **🛡️ Error Handling** Configurable error propagation with `catchErrors`, error tracking, and clear error state
23
+ - **🎨 Web Component Dashboard** — Built-in Lit-based dashboard for real-time task visualization
24
+ - **🌐 Distributed Coordination** — Abstract coordinator for multi-instance task deduplication
20
25
 
21
26
  ## 📦 Installation
22
27
 
23
28
  ```bash
24
- npm install @push.rocks/taskbuffer
25
- # or
26
29
  pnpm add @push.rocks/taskbuffer
27
30
  # or
28
- yarn add @push.rocks/taskbuffer
31
+ npm install @push.rocks/taskbuffer
29
32
  ```
30
33
 
31
34
  ## 🚀 Quick Start
32
35
 
33
- ### Basic Task Creation
36
+ ### Basic Task
34
37
 
35
38
  ```typescript
36
- import { Task, TaskManager } from '@push.rocks/taskbuffer';
39
+ import { Task } from '@push.rocks/taskbuffer';
37
40
 
38
- // Create a simple task
39
- const dataProcessor = new Task({
40
- name: 'ProcessData',
41
- taskFunction: async (data) => {
42
- console.log(`Processing: ${data}`);
43
- // Your async logic here
44
- return `Processed: ${data}`;
45
- }
41
+ const greetTask = new Task({
42
+ name: 'Greet',
43
+ taskFunction: async (name) => {
44
+ return `Hello, ${name}!`;
45
+ },
46
46
  });
47
47
 
48
- // Execute the task
49
- const result = await dataProcessor.trigger('my-data');
50
- console.log(result); // "Processed: my-data"
48
+ const result = await greetTask.trigger('World');
49
+ console.log(result); // "Hello, World!"
51
50
  ```
52
51
 
53
- ### Tasks with Progress Tracking 📊
52
+ ### Task with Steps & Progress 📊
54
53
 
55
54
  ```typescript
56
- const deploymentTask = new Task({
57
- name: 'DeployApplication',
55
+ const deployTask = new Task({
56
+ name: 'Deploy',
58
57
  steps: [
59
- { name: 'build', description: 'Building application', percentage: 30 },
58
+ { name: 'build', description: 'Building app', percentage: 30 },
60
59
  { name: 'test', description: 'Running tests', percentage: 20 },
61
60
  { name: 'deploy', description: 'Deploying to server', percentage: 40 },
62
- { name: 'verify', description: 'Verifying deployment', percentage: 10 }
63
- ] as const, // Use 'as const' for type inference
64
- taskFunction: async function() {
65
- // TypeScript knows these step names!
66
- this.notifyStep('build');
67
- await buildApplication();
68
-
69
- this.notifyStep('test');
61
+ { name: 'verify', description: 'Verifying deployment', percentage: 10 },
62
+ ] as const,
63
+ taskFunction: async () => {
64
+ deployTask.notifyStep('build');
65
+ await buildApp();
66
+
67
+ deployTask.notifyStep('test');
70
68
  await runTests();
71
-
72
- this.notifyStep('deploy');
69
+
70
+ deployTask.notifyStep('deploy');
73
71
  await deployToServer();
74
-
75
- this.notifyStep('verify');
72
+
73
+ deployTask.notifyStep('verify');
76
74
  await verifyDeployment();
77
-
75
+
78
76
  return 'Deployment successful!';
79
- }
77
+ },
80
78
  });
81
79
 
82
- // Monitor progress
83
- console.log(deploymentTask.getProgress()); // 0-100
84
- console.log(deploymentTask.getStepsMetadata()); // Detailed step info
80
+ await deployTask.trigger();
81
+ console.log(deployTask.getProgress()); // 100
82
+ console.log(deployTask.getStepsMetadata()); // Step details with status
85
83
  ```
86
84
 
85
+ > **Note:** `notifyStep()` is fully type-safe — TypeScript only accepts step names you declared in the `steps` array when you use `as const`.
86
+
87
87
  ## 🎯 Core Concepts
88
88
 
89
- ### Task Buffering - Intelligent Request Management
89
+ ### Task Buffering Intelligent Request Management
90
90
 
91
- TaskBuffer's buffering system prevents overwhelming your system with rapid-fire requests:
91
+ Prevent overwhelming your system with rapid-fire requests:
92
92
 
93
93
  ```typescript
94
94
  const apiTask = new Task({
95
95
  name: 'APIRequest',
96
+ buffered: true,
97
+ bufferMax: 5, // Maximum 5 concurrent executions
98
+ execDelay: 100, // Minimum 100ms between executions
96
99
  taskFunction: async (endpoint) => {
97
- return await fetch(endpoint).then(r => r.json());
100
+ return await fetch(endpoint).then((r) => r.json());
98
101
  },
99
- buffered: true,
100
- bufferMax: 5, // Maximum 5 concurrent executions
101
- execDelay: 100 // Minimum 100ms between executions
102
102
  });
103
103
 
104
- // Rapid fire 100 calls - only 5 will execute concurrently
104
+ // Rapid fire 100 calls only bufferMax execute concurrently
105
105
  for (let i = 0; i < 100; i++) {
106
106
  apiTask.trigger(`/api/data/${i}`);
107
107
  }
108
108
  ```
109
109
 
110
110
  **Buffer Behavior:**
111
+
111
112
  - First `bufferMax` calls execute immediately
112
113
  - Additional calls are queued
113
114
  - When buffer is full, new calls overwrite the last queued item
114
115
  - Perfect for real-time data streams where only recent data matters
115
116
 
116
- ### Task Chains - Sequential Workflows
117
+ ### Task Chains Sequential Workflows 🔗
117
118
 
118
- Build complex workflows with automatic data flow:
119
+ Build complex workflows with automatic data flow between tasks:
119
120
 
120
121
  ```typescript
121
- import { Task, Taskchain } from '@push.rocks/taskbuffer';
122
+ import { Taskchain } from '@push.rocks/taskbuffer';
122
123
 
123
124
  const fetchTask = new Task({
124
- name: 'FetchData',
125
+ name: 'Fetch',
125
126
  taskFunction: async () => {
126
- const response = await fetch('/api/data');
127
- return response.json();
128
- }
127
+ const res = await fetch('/api/data');
128
+ return res.json();
129
+ },
129
130
  });
130
131
 
131
132
  const transformTask = new Task({
132
- name: 'TransformData',
133
+ name: 'Transform',
133
134
  taskFunction: async (data) => {
134
- return data.map(item => ({
135
- ...item,
136
- transformed: true,
137
- timestamp: Date.now()
138
- }));
139
- }
135
+ return data.map((item) => ({ ...item, transformed: true }));
136
+ },
140
137
  });
141
138
 
142
139
  const saveTask = new Task({
143
- name: 'SaveData',
140
+ name: 'Save',
144
141
  taskFunction: async (transformedData) => {
145
142
  await database.save(transformedData);
146
143
  return transformedData.length;
147
- }
144
+ },
148
145
  });
149
146
 
150
- // Create and execute chain
151
- const dataChain = new Taskchain({
147
+ const pipeline = new Taskchain({
152
148
  name: 'DataPipeline',
153
- tasks: [fetchTask, transformTask, saveTask]
149
+ taskArray: [fetchTask, transformTask, saveTask],
154
150
  });
155
151
 
156
- const savedCount = await dataChain.trigger();
152
+ const savedCount = await pipeline.trigger();
157
153
  console.log(`Saved ${savedCount} items`);
158
154
  ```
159
155
 
160
- ### Parallel Execution - Concurrent Processing
156
+ Taskchain also supports dynamic mutation:
157
+
158
+ ```typescript
159
+ pipeline.addTask(newTask); // Append to chain
160
+ pipeline.removeTask(oldTask); // Remove by reference (returns boolean)
161
+ pipeline.shiftTask(); // Remove & return first task
162
+ ```
163
+
164
+ Error context is rich — a chain failure includes the chain name, failing task name, task index, and preserves the original error as `.cause`.
165
+
166
+ ### Parallel Execution — Concurrent Processing ⚡
161
167
 
162
168
  Execute multiple tasks simultaneously:
163
169
 
@@ -165,21 +171,13 @@ Execute multiple tasks simultaneously:
165
171
  import { Taskparallel } from '@push.rocks/taskbuffer';
166
172
 
167
173
  const parallel = new Taskparallel({
168
- name: 'ParallelProcessor',
169
- tasks: [
170
- emailTask,
171
- smsTask,
172
- pushNotificationTask,
173
- webhookTask
174
- ]
174
+ taskArray: [emailTask, smsTask, pushNotificationTask, webhookTask],
175
175
  });
176
176
 
177
- // All tasks execute concurrently
178
- const results = await parallel.trigger(notificationData);
179
- // results = [emailResult, smsResult, pushResult, webhookResult]
177
+ await parallel.trigger(notificationData);
180
178
  ```
181
179
 
182
- ### Debounced Tasks - Smart Trigger Coalescing
180
+ ### Debounced Tasks Smart Trigger Coalescing 🕐
183
181
 
184
182
  Coalesce rapid triggers into a single execution after a quiet period:
185
183
 
@@ -187,441 +185,573 @@ Coalesce rapid triggers into a single execution after a quiet period:
187
185
  import { TaskDebounced } from '@push.rocks/taskbuffer';
188
186
 
189
187
  const searchTask = new TaskDebounced({
190
- name: 'SearchQuery',
191
- debounceTimeInMillis: 300, // Wait 300ms after last trigger
188
+ name: 'Search',
189
+ debounceTimeInMillis: 300,
192
190
  taskFunction: async (query) => {
193
- const results = await searchAPI(query);
194
- return results;
195
- }
191
+ return await searchAPI(query);
192
+ },
196
193
  });
197
194
 
198
- // Rapid typing - only the last query executes
195
+ // Rapid calls only the last triggers after 300ms of quiet
199
196
  searchTask.trigger('h');
200
197
  searchTask.trigger('he');
201
198
  searchTask.trigger('hel');
202
- searchTask.trigger('hello'); // Only this one executes after 300ms pause
199
+ searchTask.trigger('hello'); // this one fires
203
200
  ```
204
201
 
205
- ### TaskManager - Centralized Orchestration
202
+ ### TaskOnce — Single-Execution Guard
206
203
 
207
- Manage all your tasks from a single point:
204
+ Ensure a task only runs once, regardless of how many times it's triggered:
208
205
 
209
206
  ```typescript
210
- const taskManager = new TaskManager();
207
+ import { TaskOnce } from '@push.rocks/taskbuffer';
211
208
 
212
- // Add tasks
213
- taskManager.addTask(dataProcessor);
214
- taskManager.addTask(deploymentTask);
209
+ const initTask = new TaskOnce({
210
+ name: 'Init',
211
+ taskFunction: async () => {
212
+ await setupDatabase();
213
+ console.log('Initialized!');
214
+ },
215
+ });
215
216
 
216
- // Schedule tasks with cron
217
- taskManager.addAndScheduleTask(backupTask, '0 2 * * *'); // Daily at 2 AM
218
- taskManager.addAndScheduleTask(healthCheck, '*/5 * * * *'); // Every 5 minutes
217
+ await initTask.trigger(); // Runs
218
+ await initTask.trigger(); // No-op
219
+ await initTask.trigger(); // No-op
220
+ console.log(initTask.hasTriggered); // true
221
+ ```
219
222
 
220
- // Get task metadata
221
- const metadata = taskManager.getTaskMetadata('DeployApplication');
222
- console.log(metadata);
223
- // {
224
- // name: 'DeployApplication',
225
- // status: 'idle' | 'running' | 'completed' | 'failed',
226
- // steps: [...],
227
- // currentProgress: 75,
228
- // runCount: 12,
229
- // lastRun: Date,
230
- // buffered: false,
231
- // bufferMax: undefined,
232
- // version: '1.0.0',
233
- // timeout: 30000
234
- // }
223
+ ### TaskRunner Managed Queue with Concurrency Control
235
224
 
236
- // Get all scheduled tasks
237
- const scheduled = taskManager.getScheduledTasks();
238
- scheduled.forEach(task => {
239
- console.log(`${task.name}: Next run at ${task.nextRun}`);
240
- });
225
+ Process a queue of tasks with a configurable parallelism limit:
241
226
 
242
- // Execute and remove pattern
243
- const report = await taskManager.addExecuteRemoveTask(temporaryTask, {
244
- trackProgress: true
245
- });
246
- console.log(report);
247
- // {
248
- // taskName: 'TempTask',
249
- // startTime: Date,
250
- // endTime: Date,
251
- // duration: 1523,
252
- // steps: [...],
253
- // stepsCompleted: ['step1', 'step2'],
254
- // progress: 100,
255
- // result: any,
256
- // error?: Error
257
- // }
258
- ```
227
+ ```typescript
228
+ import { TaskRunner } from '@push.rocks/taskbuffer';
259
229
 
260
- ## 🎨 Web Component Dashboard
230
+ const runner = new TaskRunner();
231
+ runner.setMaxParallelJobs(3); // Run up to 3 tasks concurrently
261
232
 
262
- Visualize your tasks in real-time with the included web component:
233
+ await runner.start();
263
234
 
264
- ```html
265
- <!DOCTYPE html>
266
- <html>
267
- <head>
268
- <script type="module">
269
- import { TaskManager } from '@push.rocks/taskbuffer';
270
- import '@push.rocks/taskbuffer/dist_ts_web/taskbuffer-dashboard.js';
271
-
272
- const taskManager = new TaskManager();
273
-
274
- // Attach to dashboard
275
- const dashboard = document.querySelector('taskbuffer-dashboard');
276
- dashboard.taskManager = taskManager;
277
- dashboard.refreshInterval = 500; // Update every 500ms
278
- </script>
279
- </head>
280
- <body>
281
- <taskbuffer-dashboard></taskbuffer-dashboard>
282
- </body>
283
- </html>
235
+ runner.addTask(taskA);
236
+ runner.addTask(taskB);
237
+ runner.addTask(taskC);
238
+ runner.addTask(taskD); // Queued until a slot opens
239
+
240
+ // When done:
241
+ await runner.stop();
284
242
  ```
285
243
 
286
- The dashboard provides:
287
- - 📊 Real-time progress bars with step indicators
288
- - 📈 Task execution history
289
- - ⏰ Scheduled task information
290
- - 🎯 Interactive task controls
291
- - 🌓 Light/dark theme support
244
+ ## 🏷️ Labels — Multi-Tenant Task Filtering
292
245
 
293
- ## 🧩 Advanced Patterns
246
+ Attach arbitrary key-value labels to any task for filtering, grouping, or multi-tenant isolation:
294
247
 
295
- ### Dynamic Task Routing
248
+ ```typescript
249
+ const task = new Task({
250
+ name: 'ProcessOrder',
251
+ labels: { userId: 'u-42', tenantId: 'acme-corp', priority: 'high' },
252
+ taskFunction: async (order) => {
253
+ /* ... */
254
+ },
255
+ });
256
+
257
+ // Manipulate labels at runtime
258
+ task.setLabel('region', 'eu-west');
259
+ task.getLabel('userId'); // 'u-42'
260
+ task.hasLabel('tenantId', 'acme-corp'); // true
261
+ task.removeLabel('priority'); // true
262
+
263
+ // Labels are included in metadata snapshots
264
+ const meta = task.getMetadata();
265
+ console.log(meta.labels); // { userId: 'u-42', tenantId: 'acme-corp', region: 'eu-west' }
266
+ ```
296
267
 
297
- Route tasks based on conditions:
268
+ ### Filtering Tasks by Label in TaskManager
298
269
 
299
270
  ```typescript
300
- const routerTask = new Task({
301
- name: 'Router',
302
- taskFunction: async (request) => {
303
- if (request.priority === 'high') {
304
- return await expressProcessor.trigger(request);
305
- } else if (request.size > 1000000) {
306
- return await bulkProcessor.trigger(request);
307
- } else {
308
- return await standardProcessor.trigger(request);
309
- }
310
- }
311
- });
271
+ const manager = new TaskManager();
272
+ manager.addTask(orderTask1); // labels: { tenantId: 'acme' }
273
+ manager.addTask(orderTask2); // labels: { tenantId: 'globex' }
274
+ manager.addTask(orderTask3); // labels: { tenantId: 'acme' }
275
+
276
+ const acmeTasks = manager.getTasksByLabel('tenantId', 'acme');
277
+ // [orderTask1, orderTask3]
278
+
279
+ const acmeMetadata = manager.getTasksMetadataByLabel('tenantId', 'acme');
280
+ // → [ITaskMetadata, ITaskMetadata]
312
281
  ```
313
282
 
314
- ### Task Pools
283
+ ## 📡 Push-Based Events — Real-Time Task Lifecycle
315
284
 
316
- Create reusable task pools for load distribution:
285
+ Every `Task` exposes an rxjs `Subject<ITaskEvent>` that emits events as the task progresses through its lifecycle:
317
286
 
318
287
  ```typescript
319
- class TaskPool {
320
- private tasks: Task[] = [];
321
- private currentIndex = 0;
322
-
323
- constructor(poolSize: number, taskConfig: any) {
324
- for (let i = 0; i < poolSize; i++) {
325
- this.tasks.push(new Task({
326
- ...taskConfig,
327
- name: `${taskConfig.name}_${i}`
328
- }));
329
- }
330
- }
331
-
332
- async execute(data: any) {
333
- const task = this.tasks[this.currentIndex];
334
- this.currentIndex = (this.currentIndex + 1) % this.tasks.length;
335
- return await task.trigger(data);
336
- }
337
- }
288
+ import type { ITaskEvent } from '@push.rocks/taskbuffer';
338
289
 
339
- const processorPool = new TaskPool(5, {
340
- name: 'DataProcessor',
341
- taskFunction: async (data) => processData(data)
290
+ const task = new Task({
291
+ name: 'DataSync',
292
+ steps: [
293
+ { name: 'fetch', description: 'Fetching data', percentage: 50 },
294
+ { name: 'save', description: 'Saving data', percentage: 50 },
295
+ ] as const,
296
+ taskFunction: async () => {
297
+ task.notifyStep('fetch');
298
+ const data = await fetchData();
299
+ task.notifyStep('save');
300
+ await saveData(data);
301
+ },
342
302
  });
303
+
304
+ // Subscribe to individual task events
305
+ task.eventSubject.subscribe((event: ITaskEvent) => {
306
+ console.log(`[${event.type}] ${event.task.name} @ ${new Date(event.timestamp).toISOString()}`);
307
+ if (event.type === 'step') console.log(` Step: ${event.stepName}`);
308
+ if (event.type === 'failed') console.log(` Error: ${event.error}`);
309
+ });
310
+
311
+ await task.trigger();
312
+ // [started] DataSync @ 2025-01-26T...
313
+ // [step] DataSync @ 2025-01-26T...
314
+ // Step: fetch
315
+ // [step] DataSync @ 2025-01-26T...
316
+ // Step: save
317
+ // [completed] DataSync @ 2025-01-26T...
343
318
  ```
344
319
 
345
- ### Error Recovery & Retry
320
+ ### Event Types
346
321
 
347
- Implement robust error handling:
322
+ | Type | When | Extra Fields |
323
+ | --- | --- | --- |
324
+ | `'started'` | Task begins execution | — |
325
+ | `'step'` | `notifyStep()` is called | `stepName` |
326
+ | `'completed'` | Task finishes successfully | — |
327
+ | `'failed'` | Task throws an error | `error` (message string) |
328
+
329
+ Every event includes a full `ITaskMetadata` snapshot (including labels) at the time of emission.
330
+
331
+ ### Aggregated Events on TaskManager
332
+
333
+ `TaskManager` automatically aggregates events from all added tasks into a single `taskSubject`:
348
334
 
349
335
  ```typescript
350
- const resilientTask = new Task({
351
- name: 'ResilientTask',
352
- taskFunction: async (data, retryCount = 0) => {
353
- try {
354
- return await riskyOperation(data);
355
- } catch (error) {
356
- if (retryCount < 3) {
357
- console.log(`Retry ${retryCount + 1}/3`);
358
- await new Promise(r => setTimeout(r, 1000 * Math.pow(2, retryCount)));
359
- return await resilientTask.trigger(data, retryCount + 1);
360
- }
361
- throw error;
362
- }
363
- }
336
+ const manager = new TaskManager();
337
+ manager.addTask(syncTask);
338
+ manager.addTask(reportTask);
339
+ manager.addTask(cleanupTask);
340
+
341
+ // Single subscription for ALL task events
342
+ manager.taskSubject.subscribe((event) => {
343
+ sendToMonitoringDashboard(event);
364
344
  });
345
+
346
+ // Events stop flowing for a task after removal
347
+ manager.removeTask(syncTask);
365
348
  ```
366
349
 
367
- ### Task Composition
350
+ `manager.stop()` automatically cleans up all event subscriptions.
351
+
352
+ ## 🛡️ Error Handling
368
353
 
369
- Compose complex behaviors from simple tasks:
354
+ By default, `trigger()` **rejects** when the task function throws — errors propagate naturally:
370
355
 
371
356
  ```typescript
372
- const compositeTask = new Task({
373
- name: 'CompositeOperation',
374
- steps: [
375
- { name: 'validate', description: 'Validating input', percentage: 20 },
376
- { name: 'process', description: 'Processing data', percentage: 60 },
377
- { name: 'notify', description: 'Sending notifications', percentage: 20 }
378
- ] as const,
379
- taskFunction: async function(data) {
380
- this.notifyStep('validate');
381
- const validated = await validationTask.trigger(data);
382
-
383
- this.notifyStep('process');
384
- const processed = await processingTask.trigger(validated);
385
-
386
- this.notifyStep('notify');
387
- await notificationTask.trigger(processed);
388
-
389
- return processed;
390
- }
357
+ const task = new Task({
358
+ name: 'RiskyOp',
359
+ taskFunction: async () => {
360
+ throw new Error('something broke');
361
+ },
391
362
  });
363
+
364
+ try {
365
+ await task.trigger();
366
+ } catch (err) {
367
+ console.error(err.message); // "something broke"
368
+ }
392
369
  ```
393
370
 
394
- ## 🔧 Configuration
371
+ ### Swallowing Errors with `catchErrors`
395
372
 
396
- ### Task Options
373
+ Set `catchErrors: true` to swallow errors and return `undefined` instead of rejecting:
397
374
 
398
375
  ```typescript
399
- interface TaskOptions<T = undefined, TSteps = []> {
400
- name?: string; // Task identifier
401
- taskFunction: Function; // Async function to execute
402
- buffered?: boolean; // Enable buffering
403
- bufferMax?: number; // Max concurrent executions
404
- execDelay?: number; // Min delay between executions
405
- timeout?: number; // Task timeout in ms
406
- version?: string; // Task version
407
- steps?: TSteps; // Progress steps configuration
408
- taskSetup?: Function; // One-time setup function
409
- beforeTask?: Function; // Runs before each execution
410
- afterTask?: Function; // Runs after each execution
411
- }
376
+ const task = new Task({
377
+ name: 'BestEffort',
378
+ catchErrors: true,
379
+ taskFunction: async () => {
380
+ throw new Error('non-critical');
381
+ },
382
+ });
383
+
384
+ const result = await task.trigger(); // undefined (no throw)
412
385
  ```
413
386
 
414
- ### TaskManager Options
387
+ ### Error State Tracking
388
+
389
+ Regardless of `catchErrors`, the task tracks errors:
415
390
 
416
391
  ```typescript
417
- const taskManager = new TaskManager({
418
- maxConcurrentTasks: 10, // Global concurrency limit
419
- defaultTimeout: 30000, // Default task timeout
420
- logLevel: 'info' // Logging verbosity
421
- });
392
+ console.log(task.lastError); // Error object (or undefined)
393
+ console.log(task.errorCount); // Number of failures across all runs
394
+ console.log(task.getMetadata().status); // 'failed'
395
+
396
+ task.clearError(); // Resets lastError to undefined (errorCount stays)
422
397
  ```
423
398
 
424
- ## 📊 Monitoring & Observability
399
+ On a subsequent successful run, `lastError` is automatically cleared.
425
400
 
426
- ### Task Events
401
+ ## 📋 TaskManager — Centralized Orchestration
427
402
 
428
403
  ```typescript
429
- task.on('started', () => console.log('Task started'));
430
- task.on('completed', (result) => console.log('Task completed:', result));
431
- task.on('failed', (error) => console.error('Task failed:', error));
432
- task.on('stepChange', (step) => console.log('Step:', step.name));
404
+ const manager = new TaskManager();
405
+
406
+ // Add tasks
407
+ manager.addTask(dataProcessor);
408
+ manager.addTask(deployTask);
409
+
410
+ // Schedule with cron expressions
411
+ manager.addAndScheduleTask(backupTask, '0 2 * * *'); // Daily at 2 AM
412
+ manager.addAndScheduleTask(healthCheck, '*/5 * * * *'); // Every 5 minutes
413
+
414
+ // Query metadata
415
+ const meta = manager.getTaskMetadata('Deploy');
416
+ console.log(meta);
417
+ // {
418
+ // name: 'Deploy',
419
+ // status: 'completed',
420
+ // steps: [...],
421
+ // currentProgress: 100,
422
+ // runCount: 3,
423
+ // labels: { env: 'production' },
424
+ // lastError: undefined,
425
+ // errorCount: 0,
426
+ // ...
427
+ // }
428
+
429
+ // All tasks at once
430
+ const allMeta = manager.getAllTasksMetadata();
431
+
432
+ // Scheduled task info
433
+ const scheduled = manager.getScheduledTasks();
434
+ const nextRuns = manager.getNextScheduledRuns(5);
435
+
436
+ // Trigger by name
437
+ await manager.triggerTaskByName('Deploy');
438
+
439
+ // One-shot: add, execute, collect report, remove
440
+ const report = await manager.addExecuteRemoveTask(temporaryTask);
441
+ console.log(report);
442
+ // {
443
+ // taskName: 'TempTask',
444
+ // startTime: 1706284800000,
445
+ // endTime: 1706284801523,
446
+ // duration: 1523,
447
+ // steps: [...],
448
+ // stepsCompleted: ['step1', 'step2'],
449
+ // progress: 100,
450
+ // result: any
451
+ // }
452
+
453
+ // Lifecycle
454
+ await manager.start(); // Starts cron scheduling + distributed coordinator
455
+ await manager.stop(); // Stops scheduling, cleans up event subscriptions
433
456
  ```
434
457
 
435
- ### Execution Metrics
458
+ ### Remove Tasks
436
459
 
437
460
  ```typescript
438
- const metrics = task.getMetrics();
439
- console.log({
440
- totalRuns: metrics.runCount,
441
- averageDuration: metrics.avgDuration,
442
- successRate: metrics.successRate,
443
- lastError: metrics.lastError
461
+ manager.removeTask(task); // Removes from map and unsubscribes event forwarding
462
+ manager.descheduleTaskByName('Deploy'); // Remove cron schedule only
463
+ ```
464
+
465
+ ## 🎨 Web Component Dashboard
466
+
467
+ Visualize your tasks in real-time with the included Lit-based web component:
468
+
469
+ ```html
470
+ <script type="module">
471
+ import { TaskManager } from '@push.rocks/taskbuffer';
472
+ import '@push.rocks/taskbuffer/dist_ts_web/taskbuffer-dashboard.js';
473
+
474
+ const manager = new TaskManager();
475
+ // ... add and schedule tasks ...
476
+
477
+ const dashboard = document.querySelector('taskbuffer-dashboard');
478
+ dashboard.taskManager = manager;
479
+ dashboard.refreshInterval = 500; // Poll every 500ms
480
+ </script>
481
+
482
+ <taskbuffer-dashboard></taskbuffer-dashboard>
483
+ ```
484
+
485
+ The dashboard provides:
486
+
487
+ - 📊 Real-time progress bars with step indicators
488
+ - 📈 Task execution history and metadata
489
+ - ⏰ Scheduled task information with next-run times
490
+ - 🌓 Light/dark theme support
491
+
492
+ ## 🌐 Distributed Coordination
493
+
494
+ For multi-instance deployments, extend `AbstractDistributedCoordinator` to prevent duplicate task execution:
495
+
496
+ ```typescript
497
+ import { TaskManager, distributedCoordination } from '@push.rocks/taskbuffer';
498
+
499
+ class RedisCoordinator extends distributedCoordination.AbstractDistributedCoordinator {
500
+ async fireDistributedTaskRequest(request) {
501
+ // Implement leader election / distributed lock via Redis
502
+ return { shouldTrigger: true, considered: true, rank: 1, reason: 'elected', ...request };
503
+ }
504
+ async updateDistributedTaskRequest(request) {
505
+ /* update status */
506
+ }
507
+ async start() {
508
+ /* connect */
509
+ }
510
+ async stop() {
511
+ /* disconnect */
512
+ }
513
+ }
514
+
515
+ const manager = new TaskManager({
516
+ distributedCoordinator: new RedisCoordinator(),
444
517
  });
445
518
  ```
446
519
 
447
- ## 🧪 Testing
520
+ When a distributed coordinator is configured, scheduled tasks consult it before executing — only the elected instance runs the task.
521
+
522
+ ## 🧩 Advanced Patterns
523
+
524
+ ### Pre-Task & After-Task Hooks
525
+
526
+ Run setup/teardown tasks automatically:
448
527
 
449
528
  ```typescript
450
- import { expect, tap } from '@git.zone/tstest';
451
- import { Task } from '@push.rocks/taskbuffer';
529
+ const mainTask = new Task({
530
+ name: 'MainWork',
531
+ preTask: new Task({
532
+ name: 'Setup',
533
+ taskFunction: async () => {
534
+ console.log('Setting up...');
535
+ },
536
+ }),
537
+ afterTask: new Task({
538
+ name: 'Cleanup',
539
+ taskFunction: async () => {
540
+ console.log('Cleaning up...');
541
+ },
542
+ }),
543
+ taskFunction: async () => {
544
+ console.log('Doing work...');
545
+ return 'done';
546
+ },
547
+ });
452
548
 
453
- tap.test('Task should execute with progress tracking', async () => {
454
- const task = new Task({
455
- name: 'TestTask',
456
- steps: [
457
- { name: 'step1', description: 'Step 1', percentage: 50 },
458
- { name: 'step2', description: 'Step 2', percentage: 50 }
459
- ] as const,
460
- taskFunction: async function() {
461
- this.notifyStep('step1');
462
- await new Promise(r => setTimeout(r, 100));
463
- this.notifyStep('step2');
464
- return 'done';
465
- }
466
- });
467
-
468
- const result = await task.trigger();
469
- expect(result).toEqual('done');
470
- expect(task.getProgress()).toEqual(100);
549
+ await mainTask.trigger();
550
+ // Setting up... Doing work... → Cleaning up...
551
+ ```
552
+
553
+ ### One-Time Setup Functions
554
+
555
+ Run an expensive initialization exactly once, before the first execution:
556
+
557
+ ```typescript
558
+ const task = new Task({
559
+ name: 'DBQuery',
560
+ taskSetup: async () => {
561
+ const pool = await createConnectionPool();
562
+ return pool; // This becomes `setupValue`
563
+ },
564
+ taskFunction: async (input, pool) => {
565
+ return await pool.query(input);
566
+ },
471
567
  });
568
+
569
+ await task.trigger('SELECT * FROM users'); // Setup runs here
570
+ await task.trigger('SELECT * FROM orders'); // Setup skipped, pool reused
472
571
  ```
473
572
 
474
- ## 🌐 Real-World Examples
573
+ ### Blocking Tasks
475
574
 
476
- ### API Rate Limiter
575
+ Make one task wait for another to finish before executing:
477
576
 
478
577
  ```typescript
479
- const apiLimiter = new Task({
480
- name: 'APIRateLimiter',
481
- buffered: true,
482
- bufferMax: 10, // Max 10 requests per second
483
- execDelay: 100, // 100ms between requests
484
- taskFunction: async (endpoint, data) => {
485
- return await fetch(endpoint, {
486
- method: 'POST',
487
- body: JSON.stringify(data)
488
- });
489
- }
578
+ const initTask = new Task({
579
+ name: 'Init',
580
+ taskFunction: async () => {
581
+ await initializeSystem();
582
+ },
490
583
  });
584
+
585
+ const workerTask = new Task({
586
+ name: 'Worker',
587
+ taskFunction: async () => {
588
+ await doWork();
589
+ },
590
+ });
591
+
592
+ workerTask.blockingTasks.push(initTask);
593
+
594
+ // Triggering worker will automatically wait for init to complete
595
+ initTask.trigger();
596
+ workerTask.trigger(); // Waits until initTask.finished resolves
491
597
  ```
492
598
 
493
599
  ### Database Migration Pipeline
494
600
 
495
601
  ```typescript
496
- const migrationChain = new Taskchain({
602
+ const migration = new Taskchain({
497
603
  name: 'DatabaseMigration',
498
- tasks: [
499
- backupDatabaseTask,
500
- validateSchemaTask,
501
- runMigrationsTask,
502
- verifyIntegrityTask,
503
- updateIndexesTask
504
- ]
604
+ taskArray: [backupTask, validateSchemaTask, runMigrationsTask, verifyIntegrityTask],
505
605
  });
506
606
 
507
- // Execute with rollback on failure
508
607
  try {
509
- await migrationChain.trigger();
608
+ await migration.trigger();
510
609
  console.log('Migration successful!');
511
610
  } catch (error) {
611
+ // error includes chain name, failing task name, index, and original cause
612
+ console.error(error.message);
512
613
  await rollbackTask.trigger();
513
- throw error;
514
614
  }
515
615
  ```
516
616
 
517
- ### Distributed Job Queue
617
+ ### Multi-Tenant SaaS Monitoring
618
+
619
+ Combine labels + events for a real-time multi-tenant dashboard:
518
620
 
519
621
  ```typescript
520
- const jobQueue = new TaskManager();
622
+ const manager = new TaskManager();
521
623
 
522
- // Worker tasks
523
- const imageProcessor = new Task({
524
- name: 'ImageProcessor',
525
- buffered: true,
526
- bufferMax: 5,
527
- steps: [
528
- { name: 'download', description: 'Downloading', percentage: 20 },
529
- { name: 'resize', description: 'Resizing', percentage: 40 },
530
- { name: 'optimize', description: 'Optimizing', percentage: 30 },
531
- { name: 'upload', description: 'Uploading', percentage: 10 }
532
- ] as const,
533
- taskFunction: async function(job) {
534
- this.notifyStep('download');
535
- const image = await downloadImage(job.url);
536
-
537
- this.notifyStep('resize');
538
- const resized = await resizeImage(image, job.dimensions);
539
-
540
- this.notifyStep('optimize');
541
- const optimized = await optimizeImage(resized);
542
-
543
- this.notifyStep('upload');
544
- return await uploadToCDN(optimized);
545
- }
546
- });
624
+ // Create tenant-scoped tasks
625
+ function createTenantTask(tenantId: string, taskName: string, fn: () => Promise<any>) {
626
+ const task = new Task({
627
+ name: `${tenantId}:${taskName}`,
628
+ labels: { tenantId },
629
+ taskFunction: fn,
630
+ });
631
+ manager.addTask(task);
632
+ return task;
633
+ }
547
634
 
548
- jobQueue.addTask(imageProcessor);
635
+ createTenantTask('acme', 'sync', async () => syncData('acme'));
636
+ createTenantTask('globex', 'sync', async () => syncData('globex'));
549
637
 
550
- // Process incoming jobs
551
- messageQueue.on('job', async (job) => {
552
- const result = await jobQueue.getTaskByName('ImageProcessor').trigger(job);
553
- await messageQueue.ack(job.id, result);
638
+ // Stream events to tenant-specific WebSocket channels
639
+ manager.taskSubject.subscribe((event) => {
640
+ const tenantId = event.task.labels?.tenantId;
641
+ if (tenantId) {
642
+ wss.broadcast(tenantId, JSON.stringify(event));
643
+ }
554
644
  });
555
- ```
556
645
 
557
- ## 🚀 Performance Tips
558
-
559
- 1. **Use Buffering Wisely** - Enable buffering for I/O-bound tasks
560
- 2. **Set Appropriate Delays** - Use `execDelay` to prevent API rate limits
561
- 3. **Leverage Task Pools** - Distribute load across multiple task instances
562
- 4. **Monitor Progress** - Use step tracking for long-running operations
563
- 5. **Clean Up** - Use `addExecuteRemoveTask` for one-time operations
646
+ // Query tasks for a specific tenant
647
+ const acmeTasks = manager.getTasksMetadataByLabel('tenantId', 'acme');
648
+ ```
564
649
 
565
- ## 🔍 Debugging
650
+ ## 📚 API Reference
566
651
 
567
- Enable detailed logging:
652
+ ### Classes
653
+
654
+ | Class | Description |
655
+ | --- | --- |
656
+ | `Task<T, TSteps>` | Core task unit with optional step tracking, labels, and event streaming |
657
+ | `TaskManager` | Centralized orchestrator with scheduling, label queries, and aggregated events |
658
+ | `Taskchain` | Sequential task executor with data flow between tasks |
659
+ | `Taskparallel` | Concurrent task executor via `Promise.all()` |
660
+ | `TaskOnce` | Single-execution guard |
661
+ | `TaskDebounced` | Debounced task using rxjs |
662
+ | `TaskRunner` | Sequential queue with configurable parallelism |
663
+ | `TaskStep` | Step tracking unit (internal, exposed via metadata) |
664
+
665
+ ### Task Methods
666
+
667
+ | Method | Returns | Description |
668
+ | --- | --- | --- |
669
+ | `trigger(input?)` | `Promise<any>` | Execute the task |
670
+ | `notifyStep(name)` | `void` | Advance to named step (type-safe) |
671
+ | `getProgress()` | `number` | Current progress 0–100 |
672
+ | `getStepsMetadata()` | `ITaskStep[]` | Step details with status |
673
+ | `getMetadata()` | `ITaskMetadata` | Full task metadata snapshot |
674
+ | `setLabel(key, value)` | `void` | Set a label |
675
+ | `getLabel(key)` | `string \| undefined` | Get a label value |
676
+ | `removeLabel(key)` | `boolean` | Remove a label |
677
+ | `hasLabel(key, value?)` | `boolean` | Check label existence / value |
678
+ | `clearError()` | `void` | Reset `lastError` to undefined |
679
+
680
+ ### Task Properties
681
+
682
+ | Property | Type | Description |
683
+ | --- | --- | --- |
684
+ | `name` | `string` | Task identifier |
685
+ | `running` | `boolean` | Whether the task is currently executing |
686
+ | `idle` | `boolean` | Inverse of `running` |
687
+ | `labels` | `Record<string, string>` | Attached labels |
688
+ | `eventSubject` | `Subject<ITaskEvent>` | rxjs Subject emitting lifecycle events |
689
+ | `lastError` | `Error \| undefined` | Last error encountered |
690
+ | `errorCount` | `number` | Total error count across all runs |
691
+ | `runCount` | `number` | Total execution count |
692
+ | `lastRun` | `Date \| undefined` | Timestamp of last execution |
693
+ | `blockingTasks` | `Task[]` | Tasks that must finish before this one starts |
694
+
695
+ ### TaskManager Methods
696
+
697
+ | Method | Returns | Description |
698
+ | --- | --- | --- |
699
+ | `addTask(task)` | `void` | Register a task (wires event forwarding) |
700
+ | `removeTask(task)` | `void` | Remove task and unsubscribe events |
701
+ | `getTaskByName(name)` | `Task \| undefined` | Look up by name |
702
+ | `triggerTaskByName(name)` | `Promise<any>` | Trigger by name |
703
+ | `addAndScheduleTask(task, cron)` | `void` | Register + schedule |
704
+ | `scheduleTaskByName(name, cron)` | `void` | Schedule existing task |
705
+ | `descheduleTaskByName(name)` | `void` | Remove schedule |
706
+ | `getTaskMetadata(name)` | `ITaskMetadata \| null` | Single task metadata |
707
+ | `getAllTasksMetadata()` | `ITaskMetadata[]` | All tasks metadata |
708
+ | `getScheduledTasks()` | `IScheduledTaskInfo[]` | Scheduled task info |
709
+ | `getNextScheduledRuns(limit?)` | `Array<{...}>` | Upcoming scheduled runs |
710
+ | `getTasksByLabel(key, value)` | `Task[]` | Filter tasks by label |
711
+ | `getTasksMetadataByLabel(key, value)` | `ITaskMetadata[]` | Filter metadata by label |
712
+ | `addExecuteRemoveTask(task, opts?)` | `Promise<ITaskExecutionReport>` | One-shot execution with report |
713
+ | `start()` | `Promise<void>` | Start cron + coordinator |
714
+ | `stop()` | `Promise<void>` | Stop cron + clean up subscriptions |
715
+
716
+ ### TaskManager Properties
717
+
718
+ | Property | Type | Description |
719
+ | --- | --- | --- |
720
+ | `taskSubject` | `Subject<ITaskEvent>` | Aggregated events from all added tasks |
721
+ | `taskMap` | `ObjectMap<Task>` | Internal task registry |
722
+
723
+ ### Exported Types
568
724
 
569
725
  ```typescript
570
- import { logger } from '@push.rocks/smartlog';
571
-
572
- logger.enableConsole();
573
- logger.level = 'debug';
574
-
575
- // Tasks will now output detailed execution logs
726
+ import type {
727
+ ITaskMetadata,
728
+ ITaskExecutionReport,
729
+ IScheduledTaskInfo,
730
+ ITaskEvent,
731
+ TTaskEventType,
732
+ ITaskStep,
733
+ ITaskFunction,
734
+ StepNames,
735
+ } from '@push.rocks/taskbuffer';
576
736
  ```
577
737
 
578
- ## 📚 API Reference
579
-
580
- ### Core Classes
581
-
582
- - **`Task<T, TSteps>`** - Basic task unit with optional step tracking
583
- - **`TaskManager`** - Central orchestrator for task management
584
- - **`Taskchain`** - Sequential task executor
585
- - **`Taskparallel`** - Concurrent task executor
586
- - **`TaskOnce`** - Single-execution task
587
- - **`TaskDebounced`** - Debounced task that waits for a pause in triggers
588
- - **`TaskRunner`** - Sequential task runner with scheduling support
589
- - **`distributedCoordination`** - Namespace for distributed task coordination
590
-
591
- ### Key Methods
592
-
593
- #### Task Methods
594
- - `trigger(input?: T): Promise<any>` - Execute the task
595
- - `notifyStep(stepName: StepNames<TSteps>): void` - Update current step
596
- - `getProgress(): number` - Get progress percentage (0-100)
597
- - `getStepsMetadata(): ITaskStep[]` - Get detailed step information
598
- - `getMetadata(): ITaskMetadata` - Get complete task metadata
599
-
600
- #### TaskManager Methods
601
- - `addTask(task: Task): void` - Register a task
602
- - `getTaskByName(name: string): Task | undefined` - Retrieve task by name
603
- - `addAndScheduleTask(task: Task, cronExpression: string): void` - Schedule task
604
- - `descheduleTaskByName(name: string): void` - Remove scheduling
605
- - `getTaskMetadata(name: string): ITaskMetadata | null` - Get task metadata
606
- - `getAllTasksMetadata(): ITaskMetadata[]` - Get all tasks metadata
607
- - `getScheduledTasks(): IScheduledTaskInfo[]` - List scheduled tasks
608
- - `addExecuteRemoveTask(task, options?): Promise<ITaskExecutionReport>` - Execute once
609
-
610
738
  ## License and Legal Information
611
739
 
612
- This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
740
+ This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
613
741
 
614
742
  **Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
615
743
 
616
744
  ### Trademarks
617
745
 
618
- This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
746
+ This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
747
+
748
+ Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
619
749
 
620
750
  ### Company Information
621
751
 
622
- Task Venture Capital GmbH
623
- Registered at District court Bremen HRB 35230 HB, Germany
752
+ Task Venture Capital GmbH
753
+ Registered at District Court Bremen HRB 35230 HB, Germany
624
754
 
625
- For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
755
+ For any legal inquiries or further information, please contact us via email at hello@task.vc.
626
756
 
627
- By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
757
+ By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.