novac 2.2.1 → 2.3.0

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.
@@ -1,125 +1,1414 @@
1
+ 'use strict';
2
+ // ============================================================
3
+ // kitasker.js — Comprehensive Task Scheduling Library
4
+ // 120+ features: classes, methods, utilities, and structures
5
+ // ============================================================
6
+
7
+ // ─── Time Units ──────────────────────────────────────────────────────────────
1
8
  const units = {
2
- ms: 1,
3
- s: 1000,
4
- m: 60 * 1000,
5
- h: 60 * 60 * 1000,
6
- day: 24 * 60 * 60 * 1000,
7
- week: 7 * 24 * 60 * 60 * 1000,
8
- month: 30 * 24 * 60 * 60 * 1000,
9
- year: 365 * 24 * 60 * 60 * 1000,
9
+ ns: 1 / 1e6,
10
+ ms: 1,
11
+ s: 1_000,
12
+ m: 60_000,
13
+ h: 3_600_000,
14
+ day: 86_400_000,
15
+ week: 604_800_000,
16
+ fortnight: 1_209_600_000,
17
+ month: 2_592_000_000,
18
+ quarter: 7_776_000_000,
19
+ year: 31_536_000_000,
20
+ decade: 315_360_000_000,
21
+ };
22
+
23
+ // ─── Task State Enum ─────────────────────────────────────────────────────────
24
+ const State = Object.freeze({
25
+ PENDING: 'pending',
26
+ RUNNING: 'running',
27
+ RESOLVED: 'resolved',
28
+ CANCELLED: 'cancelled',
29
+ FAILED: 'failed',
30
+ });
31
+
32
+ // ╔══════════════════════════════════════════════════════════════════════════╗
33
+ // ║ SECTION 1 — Utility Functions (20) ║
34
+ // ╚══════════════════════════════════════════════════════════════════════════╝
35
+
36
+ /** 1. Convert delay + unit string to milliseconds */
37
+ function parseUnit(delay, unit) {
38
+ return delay * (units[unit] ?? units.s);
39
+ }
40
+
41
+ /** 2. Format milliseconds into a human-readable string */
42
+ function humanDuration(ms) {
43
+ if (ms < 1_000) return `${ms}ms`;
44
+ if (ms < 60_000) return `${(ms / 1_000).toFixed(2)}s`;
45
+ if (ms < 3_600_000) return `${(ms / 60_000).toFixed(2)}m`;
46
+ if (ms < 86_400_000) return `${(ms / 3_600_000).toFixed(2)}h`;
47
+ return `${(ms / 86_400_000).toFixed(2)}d`;
48
+ }
49
+
50
+ /** 3. Clamp a number between min and max */
51
+ function clamp(val, min, max) {
52
+ return Math.min(Math.max(val, min), max);
53
+ }
54
+
55
+ /** 4. Generate a short unique ID */
56
+ function uid() {
57
+ return Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
58
+ }
59
+
60
+ /** 5. Deep clone a plain object/array via JSON round-trip */
61
+ function deepClone(obj) {
62
+ return JSON.parse(JSON.stringify(obj));
63
+ }
64
+
65
+ /** 6. No-operation placeholder function */
66
+ function noop() {}
67
+
68
+ /** 7. Check if a value is a thenable (Promise-like) */
69
+ function isPromise(val) {
70
+ return !!val && typeof val === 'object' && typeof val.then === 'function';
71
+ }
72
+
73
+ /** 8. Memoize a function (cache by first arg JSON key) */
74
+ function memoize(fn) {
75
+ const cache = new Map();
76
+ return function (...args) {
77
+ const key = JSON.stringify(args);
78
+ if (cache.has(key)) return cache.get(key);
79
+ const result = fn.apply(this, args);
80
+ cache.set(key, result);
81
+ return result;
10
82
  };
11
- class Task {
12
- constructor(name, fn, options = {}) {
13
- this.name = name;
14
- this.fn = fn;
15
- this.options = options;
16
- this.resolved = false;
17
- process.on('exit', () => {
18
- if (!this.resolved) {
19
- console.warn(`Task "${this.name}" was not resolved before process exit.`);
20
- }
21
- });
22
- }
23
- schedule(delay, unit) {
24
- const delayMs = delay * (units[unit] || 1000);
25
- setTimeout(() => {
26
- this.fn();
27
- this.resolved = true;
28
- }, delayMs);
29
- }
30
- interval(delay, unit) {
31
- const delayMs = delay * (units[unit] || 1000);
32
- setInterval(() => {
33
- this.fn();
34
- this.resolved = true;
35
- }, delayMs);
36
- }
37
- run() {
38
- return new Promise((resolve, reject) => {
39
- try {
40
- const result = this.fn();
41
- this.resolved = true;
42
- resolve(result);
43
- } catch (error) {
44
- reject(error);
45
- }
46
- this.remove();
47
- });
48
- }
49
- remove() {
50
- this.resolved = true;
51
- delete this;
52
- }
53
83
  }
54
- class SyncTask {
55
- constructor(name, fn, options = {}) {
56
- this.name = name;
57
- this.fn = fn;
58
- this.options = options;
59
- this.resolved = false;
60
- process.on('exit', () => {
61
- if (!this.resolved) {
62
- console.warn(`Task "${this.name}" was not resolved before process exit.`);
63
- }
64
- });
84
+
85
+ /** 9. Wrap a function so it executes at most once */
86
+ function once(fn) {
87
+ let called = false, result;
88
+ return function (...args) {
89
+ if (!called) { called = true; result = fn.apply(this, args); }
90
+ return result;
91
+ };
92
+ }
93
+
94
+ /** 10. Debounce — delays fn until `delay` ms after the last call */
95
+ function debounce(fn, delay) {
96
+ let timer;
97
+ return function (...args) {
98
+ clearTimeout(timer);
99
+ timer = setTimeout(() => fn.apply(this, args), delay);
100
+ };
101
+ }
102
+
103
+ /** 11. Throttle — fn executes at most once per `delay` ms */
104
+ function throttle(fn, delay) {
105
+ let last = 0;
106
+ return function (...args) {
107
+ const now = Date.now();
108
+ if (now - last >= delay) { last = now; return fn.apply(this, args); }
109
+ };
110
+ }
111
+
112
+ /** 12. Return a Promise that resolves after `ms` milliseconds */
113
+ function sleep(ms) {
114
+ return new Promise(r => setTimeout(r, ms));
115
+ }
116
+
117
+ /** 13. Retry an async fn up to `times` times with `delay` ms between */
118
+ async function retryFn(fn, times = 3, delay = 1_000) {
119
+ for (let i = 0; i < times; i++) {
120
+ try { return await fn(); }
121
+ catch (err) {
122
+ if (i === times - 1) throw err;
123
+ await sleep(delay);
65
124
  }
66
- schedule(delay, unit) {
67
- const delayMs = delay * (units[unit] || 1000);
68
- let execDate = delayMs + Date.now();
69
- while (true) {
70
- if (Date.now() >= execDate) {
71
- this.fn();
72
- this.resolved = true;
73
- break;
74
- }
75
- /* spin */
125
+ }
126
+ }
127
+
128
+ /** 14. Race a promise against a hard timeout */
129
+ function timeoutFn(promise, ms) {
130
+ return Promise.race([
131
+ promise,
132
+ new Promise((_, reject) =>
133
+ setTimeout(() => reject(new TaskError(`Timed out after ${ms}ms`, 'TIMEOUT')), ms)
134
+ ),
135
+ ]);
136
+ }
137
+
138
+ /** 15. Wrap a synchronous function to always return a Promise */
139
+ function wrapAsync(fn) {
140
+ return async function (...args) { return fn.apply(this, args); };
141
+ }
142
+
143
+ /** 16. Format a Date object to ISO string (defaults to now) */
144
+ function formatDate(d = new Date()) {
145
+ return d.toISOString();
146
+ }
147
+
148
+ /**
149
+ * 17. Very simple 5-field cron parser (s m h dom mon).
150
+ * Returns ms until next scheduled run.
151
+ */
152
+ function parseCron(expr) {
153
+ const fields = expr.trim().split(/\s+/);
154
+ if (fields.length !== 5) throw new TaskError('Cron must have 5 fields', 'CRON');
155
+ const [sec, min, hour, dom, mon] = fields.map(f => f === '*' ? null : parseInt(f, 10));
156
+ const now = new Date();
157
+ const next = new Date(now);
158
+ next.setMilliseconds(0);
159
+ if (sec !== null) next.setSeconds(sec); else next.setSeconds(next.getSeconds() + 1);
160
+ if (min !== null) next.setMinutes(min);
161
+ if (hour !== null) next.setHours(hour);
162
+ if (dom !== null) next.setDate(dom);
163
+ if (mon !== null) next.setMonth(mon - 1);
164
+ if (next <= now) next.setDate(next.getDate() + 1);
165
+ return next.getTime() - now.getTime();
166
+ }
167
+
168
+ /** 18. Compose multiple functions left-to-right (pipeline style) */
169
+ function compose(...fns) {
170
+ return (x) => fns.reduce((v, f) => f(v), x);
171
+ }
172
+
173
+ /** 19. Chunk an array into sub-arrays of size `n` */
174
+ function chunk(arr, n) {
175
+ const out = [];
176
+ for (let i = 0; i < arr.length; i += n) out.push(arr.slice(i, i + n));
177
+ return out;
178
+ }
179
+
180
+ /** 20. Linear interpolation between `a` and `b` by factor `t` */
181
+ function lerp(a, b, t) {
182
+ return a + (b - a) * clamp(t, 0, 1);
183
+ }
184
+
185
+ // ╔══════════════════════════════════════════════════════════════════════════╗
186
+ // ║ SECTION 2 — Core Data Classes (6) ║
187
+ // ╚══════════════════════════════════════════════════════════════════════════╝
188
+
189
+ // ─── TaskError ───────────────────────────────────────────────────────────────
190
+ /** 21. Custom error class carrying a code and optional metadata */
191
+ class TaskError extends Error {
192
+ constructor(message, code = 'TASK_ERROR', meta = {}) {
193
+ super(message);
194
+ this.name = 'TaskError';
195
+ this.code = code;
196
+ this.meta = meta;
197
+ this.timestamp = Date.now();
198
+ if (Error.captureStackTrace) Error.captureStackTrace(this, TaskError);
199
+ }
200
+ toJSON() {
201
+ return {
202
+ name: this.name, message: this.message,
203
+ code: this.code, meta: this.meta,
204
+ timestamp: this.timestamp,
205
+ };
206
+ }
207
+ }
208
+
209
+ // ─── TaskResult ──────────────────────────────────────────────────────────────
210
+ /** 22. Immutable wrapper for the outcome of a task run */
211
+ class TaskResult {
212
+ constructor(value, meta = {}) {
213
+ this.value = value;
214
+ this.meta = meta;
215
+ this.timestamp = Date.now();
216
+ this.ok = true;
217
+ this.error = null;
218
+ }
219
+
220
+ /** 23. Create a failed TaskResult */
221
+ static fail(error, meta = {}) {
222
+ const r = new TaskResult(null, meta);
223
+ r.ok = false;
224
+ r.error = error;
225
+ return r;
226
+ }
227
+
228
+ /** 24. Map the result value (no-op on failure) */
229
+ map(fn) {
230
+ if (!this.ok) return this;
231
+ return new TaskResult(fn(this.value), this.meta);
232
+ }
233
+
234
+ /** 25. FlatMap / chain with a fn that returns a TaskResult */
235
+ chain(fn) {
236
+ if (!this.ok) return this;
237
+ return fn(this.value);
238
+ }
239
+
240
+ /** 26. Provide a fallback value if this result failed */
241
+ orElse(fallback) {
242
+ return this.ok ? this.value : fallback;
243
+ }
244
+
245
+ toJSON() {
246
+ return {
247
+ ok: this.ok,
248
+ value: this.value,
249
+ error: this.error?.toJSON?.() ?? this.error,
250
+ timestamp: this.timestamp,
251
+ };
252
+ }
253
+ }
254
+
255
+ // ─── TaskStats ───────────────────────────────────────────────────────────────
256
+ /** 27. Per-task runtime statistics */
257
+ class TaskStats {
258
+ constructor() {
259
+ this.runCount = 0;
260
+ this.errorCount = 0;
261
+ this.totalTime = 0;
262
+ this.minTime = Infinity;
263
+ this.maxTime = 0;
264
+ this.lastRun = null;
265
+ this.lastError = null;
266
+ this._history = [];
267
+ this.historyMax = 100;
268
+ }
269
+
270
+ /** 28. Record one completed execution */
271
+ record(durationMs, error = null) {
272
+ this.runCount++;
273
+ this.totalTime += durationMs;
274
+ if (durationMs < this.minTime) this.minTime = durationMs;
275
+ if (durationMs > this.maxTime) this.maxTime = durationMs;
276
+ this.lastRun = Date.now();
277
+ if (error) { this.errorCount++; this.lastError = error; }
278
+ this._history.push({ duration: durationMs, error: error?.message ?? null, ts: this.lastRun });
279
+ if (this._history.length > this.historyMax) this._history.shift();
280
+ }
281
+
282
+ /** 29. Average run time in ms */
283
+ get avgTime() { return this.runCount ? this.totalTime / this.runCount : 0; }
284
+
285
+ /** 30. Success rate 0–1 */
286
+ get successRate() {
287
+ return this.runCount ? (this.runCount - this.errorCount) / this.runCount : 1;
288
+ }
289
+
290
+ /** 31. Last N history entries */
291
+ getHistory(n = this.historyMax) { return this._history.slice(-n); }
292
+
293
+ /** 32. Reset all stats to zero */
294
+ reset() { Object.assign(this, new TaskStats()); }
295
+
296
+ toJSON() {
297
+ return {
298
+ runCount: this.runCount,
299
+ errorCount: this.errorCount,
300
+ avgTime: this.avgTime,
301
+ minTime: this.minTime === Infinity ? null : this.minTime,
302
+ maxTime: this.maxTime,
303
+ successRate: this.successRate,
304
+ lastRun: this.lastRun,
305
+ lastError: this.lastError?.message ?? null,
306
+ };
307
+ }
308
+ }
309
+
310
+ // ─── TaskLogger ──────────────────────────────────────────────────────────────
311
+ /** 33. Pluggable logger with level filtering and history */
312
+ class TaskLogger {
313
+ constructor(opts = {}) {
314
+ this.prefix = opts.prefix ?? '[Tasker]';
315
+ this.level = opts.level ?? 'info'; // debug|info|warn|error|none
316
+ this.silent = opts.silent ?? false;
317
+ this.maxHistory = opts.maxHistory ?? 500;
318
+ this._history = [];
319
+ this._LEVELS = { debug: 0, info: 1, warn: 2, error: 3, none: 99 };
320
+ }
321
+
322
+ _log(lvl, ...args) {
323
+ const rank = this._LEVELS[lvl] ?? 1;
324
+ const floor = this._LEVELS[this.level] ?? 1;
325
+ const entry = { level: lvl, message: args.join(' '), ts: Date.now() };
326
+ this._history.push(entry);
327
+ if (this._history.length > this.maxHistory) this._history.shift();
328
+ if (this.silent || rank < floor) return;
329
+ (console[lvl] ?? console.log)(`${this.prefix} [${lvl.toUpperCase()}]`, ...args);
330
+ }
331
+
332
+ /** 34. Debug-level log */
333
+ debug(...a) { this._log('debug', ...a); }
334
+ /** 35. Info-level log */
335
+ info(...a) { this._log('info', ...a); }
336
+ /** 36. Warn-level log */
337
+ warn(...a) { this._log('warn', ...a); }
338
+ /** 37. Error-level log */
339
+ error(...a) { this._log('error', ...a); }
340
+ /** 38. Get a copy of the log history */
341
+ getHistory() { return [...this._history]; }
342
+ /** 39. Clear log history */
343
+ clearHistory() { this._history = []; }
344
+ }
345
+
346
+ // ─── TaskEventEmitter ────────────────────────────────────────────────────────
347
+ /** 40. Minimal typed event emitter used as base for all task classes */
348
+ class TaskEventEmitter {
349
+ constructor() { this._listeners = {}; }
350
+
351
+ /** 41. Subscribe to an event */
352
+ on(event, fn) {
353
+ (this._listeners[event] ??= []).push(fn);
354
+ return this;
355
+ }
356
+
357
+ /** 42. Subscribe for a single emission only */
358
+ once(event, fn) {
359
+ const w = (...a) => { fn(...a); this.off(event, w); };
360
+ return this.on(event, w);
361
+ }
362
+
363
+ /** 43. Unsubscribe a specific listener */
364
+ off(event, fn) {
365
+ this._listeners[event] = (this._listeners[event] ?? []).filter(l => l !== fn);
366
+ return this;
367
+ }
368
+
369
+ /** 44. Emit an event to all subscribers (also fires wildcard '*') */
370
+ emit(event, ...args) {
371
+ (this._listeners[event] ?? []).forEach(fn => fn(...args));
372
+ (this._listeners['*'] ?? []).forEach(fn => fn(event, ...args));
373
+ return this;
374
+ }
375
+
376
+ /** 45. Remove all listeners for one event, or all events */
377
+ removeAllListeners(event) {
378
+ if (event) delete this._listeners[event];
379
+ else this._listeners = {};
380
+ return this;
381
+ }
382
+
383
+ /** 46. List all event names with at least one listener */
384
+ eventNames() { return Object.keys(this._listeners); }
385
+ }
386
+
387
+ // ─── TaskMiddlewarePipeline ───────────────────────────────────────────────────
388
+ /** 47. Koa-style async middleware pipeline for before/after hooks */
389
+ class TaskMiddlewarePipeline {
390
+ constructor() { this._mw = []; }
391
+
392
+ /** 48. Register an async middleware fn(ctx, next) */
393
+ use(fn) { this._mw.push(fn); return this; }
394
+
395
+ /** 49. Execute pipeline with a shared context object */
396
+ async run(ctx) {
397
+ let i = 0;
398
+ const next = async () => {
399
+ if (i < this._mw.length) await this._mw[i++](ctx, next);
400
+ };
401
+ await next();
402
+ return ctx;
403
+ }
404
+ }
405
+
406
+ // ╔══════════════════════════════════════════════════════════════════════════╗
407
+ // ║ SECTION 3 — Core Task Classes (Task + SyncTask, upgraded) ║
408
+ // ╚══════════════════════════════════════════════════════════════════════════╝
409
+
410
+ // ─── Task ────────────────────────────────────────────────────────────────────
411
+ /**
412
+ * 50. Async-friendly task with full lifecycle: state, hooks, retries,
413
+ * timeout, tags, dependencies, scheduling, and serialisation.
414
+ */
415
+ class Task extends TaskEventEmitter {
416
+ constructor(name, fn, options = {}) {
417
+ super();
418
+ this.id = uid();
419
+ this.name = name;
420
+ this.fn = fn;
421
+ this.options = options;
422
+ this.stats = new TaskStats();
423
+ this._state = State.PENDING;
424
+ this._tags = new Set(options.tags ?? []);
425
+ this._meta = {};
426
+ this._hooks = { before: [], after: [], error: [], cancel: [] };
427
+ this._timers = [];
428
+ this._runCount = 0;
429
+ this._maxRuns = options.maxRuns ?? Infinity;
430
+ this._locked = false;
431
+ this._retries = options.retries ?? 0;
432
+ this._retryDelay = options.retryDelay ?? 1_000;
433
+ this._timeoutMs = options.timeout ?? null;
434
+ this._deps = [];
435
+ this._logger = options.logger ?? null;
436
+
437
+ process.on('exit', () => {
438
+ if (this._state === State.PENDING || this._state === State.RUNNING) {
439
+ process.stderr.write(`[kitasker] Task "${this.name}" exited in state "${this._state}"\n`);
76
440
  }
441
+ });
442
+ }
443
+
444
+ // ── State Queries ──────────────────────────────────────────────────────
445
+ /** 51. True when task has not yet started */
446
+ isPending() { return this._state === State.PENDING; }
447
+ /** 52. True while the fn is executing */
448
+ isRunning() { return this._state === State.RUNNING; }
449
+ /** 53. True after a successful run */
450
+ isResolved() { return this._state === State.RESOLVED; }
451
+ /** 54. True after cancel() was called */
452
+ isCancelled() { return this._state === State.CANCELLED; }
453
+ /** 55. True if the last run threw and all retries failed */
454
+ isFailed() { return this._state === State.FAILED; }
455
+ /** 56. Current state string */
456
+ getState() { return this._state; }
457
+
458
+ // ── Metadata & Tags ────────────────────────────────────────────────────
459
+ /** 57. Store an arbitrary key-value pair on this task */
460
+ setMeta(key, value) { this._meta[key] = value; return this; }
461
+ /** 58. Retrieve a stored metadata value */
462
+ getMeta(key) { return this._meta[key]; }
463
+ /** 59. Attach one or more tags */
464
+ tag(...tags) { tags.forEach(t => this._tags.add(t)); return this; }
465
+ /** 60. Check for a specific tag */
466
+ hasTag(t) { return this._tags.has(t); }
467
+ /** 61. Return all tags as an array */
468
+ getTags() { return [...this._tags]; }
469
+
470
+ // ── Lifecycle Hooks ────────────────────────────────────────────────────
471
+ /** 62. Run a callback immediately before execution */
472
+ onStart(fn) { this._hooks.before.push(fn); return this; }
473
+ /** 63. Run a callback after successful execution */
474
+ onComplete(fn) { this._hooks.after.push(fn); return this; }
475
+ /** 64. Run a callback on error (after all retries) */
476
+ onError(fn) { this._hooks.error.push(fn); return this; }
477
+ /** 65. Run a callback when cancel() is called */
478
+ onCancel(fn) { this._hooks.cancel.push(fn); return this; }
479
+
480
+ // ── Configuration Chaining ─────────────────────────────────────────────
481
+ /** 66. Set retry count and optional delay between retries */
482
+ retry(n, delayMs = 1_000) { this._retries = n; this._retryDelay = delayMs; return this; }
483
+ /** 67. Fail task if it exceeds this many ms */
484
+ timeout(ms) { this._timeoutMs = ms; return this; }
485
+ /** 68. Limit total runs (interval/recurring respect this) */
486
+ times(n) { this._maxRuns = n; return this; }
487
+ /** 69. Convenience: run exactly once, then auto-cancel on interval */
488
+ once() { return this.times(1); }
489
+ /** 70. Prevent run() from executing */
490
+ lock() { this._locked = true; return this; }
491
+ /** 71. Allow run() again */
492
+ unlock() { this._locked = false; return this; }
493
+ /** 72. Declare upstream Task dependencies (await them before running) */
494
+ depends(...tasks) { this._deps.push(...tasks); return this; }
495
+
496
+ // ── Internal Helpers ───────────────────────────────────────────────────
497
+ async _runHooks(hooks, ...args) {
498
+ for (const h of hooks) await h(...args);
499
+ }
500
+ _trackTimer(t) { this._timers.push(t); return t; }
501
+
502
+ // ── Core Execution ─────────────────────────────────────────────────────
503
+ /** 73. Execute the task asynchronously; returns a TaskResult */
504
+ async run() {
505
+ if (this._locked)
506
+ throw new TaskError(`Task "${this.name}" is locked.`, 'LOCKED');
507
+ if (this._runCount >= this._maxRuns)
508
+ throw new TaskError(`Task "${this.name}" reached max runs (${this._maxRuns}).`, 'MAX_RUNS');
509
+ if (this._state === State.CANCELLED)
510
+ throw new TaskError(`Task "${this.name}" is cancelled.`, 'CANCELLED');
511
+
512
+ // Resolve dependencies first
513
+ for (const dep of this._deps) {
514
+ if (!dep.isResolved()) await dep.run();
77
515
  }
78
- interval(delay, unit) {
79
- const delayMs = delay * (units[unit] || 1000);
80
- let execDate = delayMs + Date.now();
81
- while (true) {
82
- if (Date.now() >= execDate) {
83
- execDate += delayMs;
84
- this.fn();
85
- this.resolved = true;
516
+
517
+ this._state = State.RUNNING;
518
+ this._runCount++;
519
+ this.emit('start', this);
520
+ this._logger?.debug(`▶ ${this.name} starting (run #${this._runCount})`);
521
+ await this._runHooks(this._hooks.before, this);
522
+
523
+ const t0 = Date.now();
524
+ let result, error;
525
+ const maxAttempts = this._retries + 1;
526
+
527
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
528
+ try {
529
+ let p = Promise.resolve().then(() => this.fn(this));
530
+ if (this._timeoutMs) p = timeoutFn(p, this._timeoutMs);
531
+ result = await p;
532
+ error = null;
533
+ break;
534
+ } catch (err) {
535
+ error = err instanceof TaskError
536
+ ? err
537
+ : new TaskError(err.message, 'EXEC', { original: err });
538
+ if (attempt < maxAttempts) {
539
+ this._logger?.warn(`↺ ${this.name} retry ${attempt}/${this._retries}: ${error.message}`);
540
+ this.emit('retry', this, attempt, error);
541
+ await sleep(this._retryDelay);
86
542
  }
87
- /* spin */
88
543
  }
89
544
  }
90
- run() {
91
- let result = null;
92
- try {
93
- result = this.fn();
94
- this.resolved = true;
95
- } catch (error) {
96
- throw error;
97
- }
98
- this.remove();
99
- return result;
545
+
546
+ const duration = Date.now() - t0;
547
+ this.stats.record(duration, error);
548
+
549
+ if (error) {
550
+ this._state = State.FAILED;
551
+ this._logger?.error(`✗ ${this.name} failed in ${humanDuration(duration)}: ${error.message}`);
552
+ await this._runHooks(this._hooks.error, error, this);
553
+ this.emit('error', error, this);
554
+ return TaskResult.fail(error, { duration, task: this.name });
100
555
  }
101
- remove() {
102
- this.resolved = true;
103
- delete this;
556
+
557
+ this._state = State.RESOLVED;
558
+ this._logger?.info(`✓ ${this.name} done in ${humanDuration(duration)}`);
559
+ const tr = new TaskResult(result, { duration, task: this.name });
560
+ await this._runHooks(this._hooks.after, tr, this);
561
+ this.emit('complete', tr, this);
562
+ return tr;
563
+ }
564
+
565
+ /** 74. Schedule a single delayed execution */
566
+ schedule(delay, unit) {
567
+ const ms = parseUnit(delay, unit);
568
+ this._trackTimer(setTimeout(() => this.run(), ms));
569
+ return this;
570
+ }
571
+
572
+ /** 75. Schedule a repeating execution; respects maxRuns */
573
+ interval(delay, unit) {
574
+ const ms = parseUnit(delay, unit);
575
+ const t = setInterval(() => {
576
+ if (this._runCount >= this._maxRuns) { clearInterval(t); return; }
577
+ this.run();
578
+ }, ms);
579
+ this._trackTimer(t);
580
+ return this;
581
+ }
582
+
583
+ /** 76. Cancel all pending timers and mark the task cancelled */
584
+ cancel() {
585
+ this._timers.forEach(t => { clearTimeout(t); clearInterval(t); });
586
+ this._timers = [];
587
+ if (this._state !== State.RESOLVED) this._state = State.CANCELLED;
588
+ this._runHooks(this._hooks.cancel, this);
589
+ this.emit('cancel', this);
590
+ return this;
591
+ }
592
+
593
+ /** 77. Reset state so the task can be run again */
594
+ reset() {
595
+ this._state = State.PENDING;
596
+ this._runCount = 0;
597
+ this.stats.reset();
598
+ return this;
599
+ }
600
+
601
+ /** 78. Clone this task with a new ID */
602
+ clone() {
603
+ const t = new Task(`${this.name}_clone`, this.fn, deepClone(this.options));
604
+ t._retries = this._retries;
605
+ t._retryDelay = this._retryDelay;
606
+ t._timeoutMs = this._timeoutMs;
607
+ t._maxRuns = this._maxRuns;
608
+ this.getTags().forEach(tag => t.tag(tag));
609
+ return t;
610
+ }
611
+
612
+ /** 79. Serialize this task's configuration and stats */
613
+ toJSON() {
614
+ return {
615
+ id: this.id,
616
+ name: this.name,
617
+ state: this._state,
618
+ tags: this.getTags(),
619
+ meta: this._meta,
620
+ runCount: this._runCount,
621
+ maxRuns: this._maxRuns,
622
+ retries: this._retries,
623
+ timeout: this._timeoutMs,
624
+ stats: this.stats.toJSON(),
625
+ };
626
+ }
627
+ }
628
+
629
+ // ─── SyncTask ────────────────────────────────────────────────────────────────
630
+ /** 80. Synchronous task — blocks the event loop; only for very fast ops */
631
+ class SyncTask extends TaskEventEmitter {
632
+ constructor(name, fn, options = {}) {
633
+ super();
634
+ this.id = uid();
635
+ this.name = name;
636
+ this.fn = fn;
637
+ this.options = options;
638
+ this.stats = new TaskStats();
639
+ this._state = State.PENDING;
640
+ this._meta = {};
641
+
642
+ process.on('exit', () => {
643
+ if (this._state === State.PENDING)
644
+ process.stderr.write(`[kitasker] SyncTask "${this.name}" never ran.\n`);
645
+ });
646
+ }
647
+
648
+ /** 81. Execute synchronously; returns a TaskResult */
649
+ run() {
650
+ const t0 = Date.now();
651
+ this._state = State.RUNNING;
652
+ this.emit('start', this);
653
+ try {
654
+ const result = this.fn(this);
655
+ this._state = State.RESOLVED;
656
+ const d = Date.now() - t0;
657
+ this.stats.record(d);
658
+ this.emit('complete', result, this);
659
+ return new TaskResult(result, { duration: d, task: this.name });
660
+ } catch (err) {
661
+ this._state = State.FAILED;
662
+ const e = new TaskError(err.message, 'SYNC_EXEC');
663
+ const d = Date.now() - t0;
664
+ this.stats.record(d, e);
665
+ this.emit('error', e, this);
666
+ return TaskResult.fail(e, { task: this.name });
104
667
  }
668
+ }
669
+
670
+ /** 82. Spin-wait then run once */
671
+ schedule(delay, unit) {
672
+ const end = Date.now() + parseUnit(delay, unit);
673
+ while (Date.now() < end) { /* busy-wait */ }
674
+ return this.run();
675
+ }
676
+
677
+ /** 83. Spin-wait interval (blocks forever — only for extremely short intervals) */
678
+ interval(delay, unit) {
679
+ const ms = parseUnit(delay, unit);
680
+ let next = Date.now() + ms;
681
+ while (true) { if (Date.now() >= next) { this.run(); next += ms; } }
682
+ }
683
+
684
+ /** 84. Set a metadata key */
685
+ setMeta(k, v) { this._meta[k] = v; return this; }
686
+ /** 85. Get a metadata key */
687
+ getMeta(k) { return this._meta[k]; }
688
+
689
+ toJSON() {
690
+ return { id: this.id, name: this.name, state: this._state, stats: this.stats.toJSON() };
691
+ }
105
692
  }
106
693
 
107
- class Tasker {
108
- constructor(name, opts = {}) {
109
- this.name = name;
110
- this.options = opts;
694
+ // ╔══════════════════════════════════════════════════════════════════════════╗
695
+ // ║ SECTION 4 — Specialised Task Subclasses (8)
696
+ // ╚══════════════════════════════════════════════════════════════════════════╝
697
+
698
+ // ─── RetryTask ───────────────────────────────────────────────────────────────
699
+ /** 86. Task with configurable retry backoff strategies */
700
+ class RetryTask extends Task {
701
+ constructor(name, fn, options = {}) {
702
+ super(name, fn, { retries: 3, retryDelay: 500, ...options });
703
+ this._backoff = options.backoff ?? 'linear'; // 'fixed' | 'linear' | 'exponential'
704
+ }
705
+
706
+ /** 87. Set backoff strategy: 'fixed' | 'linear' | 'exponential' */
707
+ backoff(strategy) { this._backoff = strategy; return this; }
708
+
709
+ /** 88. Compute delay for a given attempt number */
710
+ _nextDelay(attempt) {
711
+ if (this._backoff === 'exponential') return this._retryDelay * (2 ** (attempt - 1));
712
+ if (this._backoff === 'linear') return this._retryDelay * attempt;
713
+ return this._retryDelay;
714
+ }
715
+ }
716
+
717
+ // ─── TimeoutTask ─────────────────────────────────────────────────────────────
718
+ /** 89. Task that automatically fails if execution exceeds a time limit */
719
+ class TimeoutTask extends Task {
720
+ constructor(name, fn, ms, options = {}) {
721
+ super(name, fn, { timeout: ms, ...options });
722
+ }
723
+
724
+ /** 90. Update timeout value at runtime */
725
+ setTimeout(ms) { this._timeoutMs = ms; return this; }
726
+ }
727
+
728
+ // ─── DebouncedTask ───────────────────────────────────────────────────────────
729
+ /** 91. Task whose run() is debounced — only fires after a quiet period */
730
+ class DebouncedTask extends Task {
731
+ constructor(name, fn, delay, options = {}) {
732
+ super(name, fn, options);
733
+ this._debounceDelay = delay;
734
+ this._debounced = debounce(() => super.run(), delay);
735
+ }
736
+
737
+ /** 92. Debounced run() */
738
+ run() { return this._debounced(); }
739
+
740
+ /** 93. Update debounce delay and rebuild the debounced wrapper */
741
+ setDelay(ms) {
742
+ this._debounceDelay = ms;
743
+ this._debounced = debounce(() => super.run(), ms);
744
+ return this;
745
+ }
746
+ }
747
+
748
+ // ─── ThrottledTask ───────────────────────────────────────────────────────────
749
+ /** 94. Task whose run() is throttled to at most once per interval */
750
+ class ThrottledTask extends Task {
751
+ constructor(name, fn, delay, options = {}) {
752
+ super(name, fn, options);
753
+ this._throttleDelay = delay;
754
+ this._throttled = throttle(() => super.run(), delay);
755
+ }
756
+
757
+ /** 95. Throttled run() */
758
+ run() { return this._throttled(); }
759
+
760
+ /** 96. Update throttle delay */
761
+ setDelay(ms) {
762
+ this._throttleDelay = ms;
763
+ this._throttled = throttle(() => super.run(), ms);
764
+ return this;
765
+ }
766
+ }
767
+
768
+ // ─── ConditionalTask ─────────────────────────────────────────────────────────
769
+ /** 97. Task that only executes when a guard function returns truthy */
770
+ class ConditionalTask extends Task {
771
+ constructor(name, fn, condition, options = {}) {
772
+ super(name, fn, options);
773
+ this._condition = condition;
774
+ }
775
+
776
+ /** 98. Check condition before delegating to super.run() */
777
+ async run() {
778
+ const ok = await Promise.resolve(this._condition());
779
+ if (!ok) {
780
+ this.emit('skip', this);
781
+ return TaskResult.fail(
782
+ new TaskError('Condition not met', 'CONDITION'),
783
+ { task: this.name }
784
+ );
111
785
  }
112
- createTask(name, fn, opts) {
113
- return new Task(name, fn, opts);
786
+ return super.run();
787
+ }
788
+
789
+ /** 99. Replace the guard function at runtime */
790
+ setCondition(fn) { this._condition = fn; return this; }
791
+ }
792
+
793
+ // ─── PriorityTask ────────────────────────────────────────────────────────────
794
+ /** 100. Task with a numeric priority (higher = run first in queues) */
795
+ class PriorityTask extends Task {
796
+ constructor(name, fn, priority = 0, options = {}) {
797
+ super(name, fn, options);
798
+ this.priority = priority;
799
+ }
800
+
801
+ /** 101. Update priority value */
802
+ setPriority(n) { this.priority = n; return this; }
803
+ }
804
+
805
+ // ─── RecurringTask ───────────────────────────────────────────────────────────
806
+ /** 102. Interval-based task with pause/resume/stop controls */
807
+ class RecurringTask extends Task {
808
+ constructor(name, fn, delay, unit = 'ms', options = {}) {
809
+ super(name, fn, options);
810
+ this._delay = parseUnit(delay, unit);
811
+ this._paused = false;
812
+ this._timer = null;
813
+ this._runLimit = options.runLimit ?? Infinity;
814
+ }
815
+
816
+ /** 103. Start the recurring interval */
817
+ start() {
818
+ this._timer = setInterval(async () => {
819
+ if (this._paused) return;
820
+ if (this._runCount >= this._runLimit) { this.stop(); return; }
821
+ await this.run();
822
+ }, this._delay);
823
+ this._trackTimer(this._timer);
824
+ this.emit('start', this);
825
+ return this;
826
+ }
827
+
828
+ /** 104. Pause (timer ticks, fn is skipped) */
829
+ pause() { this._paused = true; this.emit('pause', this); return this; }
830
+ /** 105. Resume after pause */
831
+ resume() { this._paused = false; this.emit('resume', this); return this; }
832
+ /** 106. Stop the interval entirely */
833
+ stop() { clearInterval(this._timer); this.emit('stop', this); return this; }
834
+
835
+ /** 107. Change the interval delay (restarts the timer) */
836
+ setDelay(delay, unit = 'ms') {
837
+ this._delay = parseUnit(delay, unit);
838
+ this.stop();
839
+ return this.start();
840
+ }
841
+ }
842
+
843
+ // ─── CronTask ────────────────────────────────────────────────────────────────
844
+ /** 108. Task driven by a 5-field cron expression */
845
+ class CronTask extends Task {
846
+ constructor(name, fn, cronExpr, options = {}) {
847
+ super(name, fn, options);
848
+ this._expr = cronExpr;
849
+ this._timer = null;
850
+ }
851
+
852
+ /** 109. Start the cron loop */
853
+ start() {
854
+ const schedule = () => {
855
+ try {
856
+ const ms = parseCron(this._expr);
857
+ this._timer = setTimeout(async () => { await this.run(); schedule(); }, ms);
858
+ this._trackTimer(this._timer);
859
+ } catch (e) { this.emit('error', e, this); }
860
+ };
861
+ schedule();
862
+ return this;
863
+ }
864
+
865
+ /** 110. Stop the cron and cancel the task */
866
+ stop() {
867
+ clearTimeout(this._timer);
868
+ this.cancel();
869
+ return this;
870
+ }
871
+
872
+ /** 111. Swap cron expression and restart */
873
+ setExpr(expr) {
874
+ this._expr = expr;
875
+ this.stop();
876
+ this._state = State.PENDING;
877
+ return this.start();
878
+ }
879
+ }
880
+
881
+ // ╔══════════════════════════════════════════════════════════════════════════╗
882
+ // ║ SECTION 5 — Composite / Structural Classes (8) ║
883
+ // ╚══════════════════════════════════════════════════════════════════════════╝
884
+
885
+ // ─── BatchTask ───────────────────────────────────────────────────────────────
886
+ /** 112. Runs an array of items through a function in configurable batches */
887
+ class BatchTask extends TaskEventEmitter {
888
+ constructor(name, items, fn, options = {}) {
889
+ super();
890
+ this.id = uid();
891
+ this.name = name;
892
+ this.items = items;
893
+ this.fn = fn;
894
+ this.batchSize = options.batchSize ?? 10;
895
+ this.delay = options.delay ?? 0;
896
+ this.stats = new TaskStats();
897
+ this._state = State.PENDING;
898
+ }
899
+
900
+ /** 113. Execute all items in batches; resolves with allSettled array */
901
+ async run() {
902
+ this._state = State.RUNNING;
903
+ const results = [];
904
+ const batches = chunk(this.items, this.batchSize);
905
+ const t0 = Date.now();
906
+
907
+ for (const [i, batch] of batches.entries()) {
908
+ this.emit('batch', i, batch);
909
+ const r = await Promise.allSettled(batch.map(item => this.fn(item)));
910
+ results.push(...r);
911
+ if (this.delay && i < batches.length - 1) await sleep(this.delay);
114
912
  }
115
- createSyncTask(name, fn, opts) {
116
- return new SyncTask(name, fn, opts);
913
+
914
+ this._state = State.RESOLVED;
915
+ this.stats.record(Date.now() - t0);
916
+ this.emit('complete', results);
917
+ return results;
918
+ }
919
+
920
+ /** 114. Set items batch size */
921
+ setBatchSize(n) { this.batchSize = n; return this; }
922
+ /** 115. Set delay between batches in ms */
923
+ setBatchDelay(ms){ this.delay = ms; return this; }
924
+ }
925
+
926
+ // ─── TaskChain ───────────────────────────────────────────────────────────────
927
+ /** 116. Runs Tasks sequentially, threading results forward */
928
+ class TaskChain extends TaskEventEmitter {
929
+ constructor(name) {
930
+ super();
931
+ this.id = uid();
932
+ this.name = name;
933
+ this._chain = [];
934
+ this._err = null;
935
+ this.stats = new TaskStats();
936
+ this._state = State.PENDING;
937
+ }
938
+
939
+ /** 117. Append a task to the chain */
940
+ then(task) { this._chain.push(task); return this; }
941
+ /** 118. Attach an error handler (stops re-throw on failure) */
942
+ catch(fn) { this._err = fn; return this; }
943
+
944
+ /** 119. Run every task in sequence; each receives the previous result */
945
+ async run() {
946
+ this._state = State.RUNNING;
947
+ let prev = undefined;
948
+ const t0 = Date.now();
949
+
950
+ for (const task of this._chain) {
951
+ try {
952
+ task.setMeta?.('prevResult', prev);
953
+ const tr = await task.run();
954
+ prev = tr.value;
955
+ this.emit('step', task, tr);
956
+ } catch (err) {
957
+ this._state = State.FAILED;
958
+ if (this._err) { this._err(err, task); return undefined; }
959
+ throw err;
960
+ }
117
961
  }
118
- removeTask(task) {
119
- task.remove();
962
+
963
+ this._state = State.RESOLVED;
964
+ this.stats.record(Date.now() - t0);
965
+ this.emit('complete', prev);
966
+ return prev;
967
+ }
968
+
969
+ /** 120. Number of tasks in the chain */
970
+ get length() { return this._chain.length; }
971
+ }
972
+
973
+ // ─── TaskPipeline ────────────────────────────────────────────────────────────
974
+ /** Pipes the output of each function as the input to the next */
975
+ class TaskPipeline extends TaskEventEmitter {
976
+ constructor(name) {
977
+ super();
978
+ this.id = uid();
979
+ this.name = name;
980
+ this._fns = [];
981
+ this.stats = new TaskStats();
982
+ }
983
+
984
+ /** Add a transform step */
985
+ pipe(fn) { this._fns.push(fn); return this; }
986
+
987
+ /** Run the pipeline from an initial value */
988
+ async run(initial) {
989
+ const t0 = Date.now();
990
+ let val = initial;
991
+ for (const fn of this._fns) {
992
+ val = await Promise.resolve(fn(val));
993
+ this.emit('step', fn, val);
120
994
  }
121
- scheduleTask(task, delay, unit) {
122
- task.schedule(delay, unit);
995
+ this.stats.record(Date.now() - t0);
996
+ this.emit('complete', val);
997
+ return val;
998
+ }
999
+ }
1000
+
1001
+ // ─── TaskGroup ───────────────────────────────────────────────────────────────
1002
+ /** Group of tasks runnable as all / race / any / settled / sequential */
1003
+ class TaskGroup extends TaskEventEmitter {
1004
+ constructor(name, tasks = []) {
1005
+ super();
1006
+ this.id = uid();
1007
+ this.name = name;
1008
+ this._tasks = [...tasks];
1009
+ }
1010
+
1011
+ /** Add a task to the group */
1012
+ add(task) { this._tasks.push(task); return this; }
1013
+ /** Remove task by name */
1014
+ remove(name) { this._tasks = this._tasks.filter(t => t.name !== name); return this; }
1015
+
1016
+ /** Run all in parallel (Promise.all) */
1017
+ all() { return Promise.all( this._tasks.map(t => t.run())); }
1018
+ /** Race — first to resolve or reject wins */
1019
+ race() { return Promise.race( this._tasks.map(t => t.run())); }
1020
+ /** All results regardless of individual failure */
1021
+ settled() { return Promise.allSettled(this._tasks.map(t => t.run())); }
1022
+ /** First to resolve successfully (Promise.any) */
1023
+ any() { return Promise.any( this._tasks.map(t => t.run())); }
1024
+
1025
+ /** Run tasks one after another */
1026
+ async sequential() {
1027
+ const results = [];
1028
+ for (const t of this._tasks) results.push(await t.run());
1029
+ return results;
1030
+ }
1031
+
1032
+ /** Get tasks filtered by tag */
1033
+ byTag(tag) { return this._tasks.filter(t => t.hasTag?.(tag)); }
1034
+ /** Cancel every task in the group */
1035
+ cancelAll() { this._tasks.forEach(t => t.cancel?.()); return this; }
1036
+ }
1037
+
1038
+ // ─── TaskQueue ───────────────────────────────────────────────────────────────
1039
+ /** FIFO queue processing tasks with a configurable concurrency limit */
1040
+ class TaskQueue extends TaskEventEmitter {
1041
+ constructor(name, opts = {}) {
1042
+ super();
1043
+ this.id = uid();
1044
+ this.name = name;
1045
+ this.concurrency = opts.concurrency ?? 1;
1046
+ this._queue = [];
1047
+ this._running = 0;
1048
+ this._paused = false;
1049
+ }
1050
+
1051
+ /** Add a task to the queue */
1052
+ enqueue(task) {
1053
+ if (this.concurrency > 1) {
1054
+ // priority-aware sort if tasks carry priority
1055
+ const p = task.priority ?? 0;
1056
+ let i = this._queue.findIndex(t => (t.priority ?? 0) < p);
1057
+ if (i === -1) i = this._queue.length;
1058
+ this._queue.splice(i, 0, task);
1059
+ } else {
1060
+ this._queue.push(task);
1061
+ }
1062
+ this.emit('enqueue', task);
1063
+ this._tick();
1064
+ return this;
1065
+ }
1066
+
1067
+ _tick() {
1068
+ while (!this._paused && this._running < this.concurrency && this._queue.length) {
1069
+ const task = this._queue.shift();
1070
+ this._running++;
1071
+ this.emit('dequeue', task);
1072
+ Promise.resolve(task.run())
1073
+ .then(r => { this._running--; this.emit('done', task, r); this._tick(); })
1074
+ .catch(e => { this._running--; this.emit('error', task, e); this._tick(); });
123
1075
  }
1076
+ if (!this._queue.length && !this._running) this.emit('drain');
1077
+ }
1078
+
1079
+ /** Pause queue processing */
1080
+ pause() { this._paused = true; this.emit('pause'); return this; }
1081
+ /** Resume queue processing */
1082
+ resume() { this._paused = false; this.emit('resume'); this._tick(); return this; }
1083
+ /** Pending items count */
1084
+ get size() { return this._queue.length; }
1085
+ /** Currently running count */
1086
+ get active() { return this._running; }
1087
+ /** Discard all pending (not currently running) tasks */
1088
+ clear() { this._queue = []; return this; }
1089
+ }
1090
+
1091
+ // ─── TaskPool ────────────────────────────────────────────────────────────────
1092
+ /** Fixed-size slot pool — limits maximum concurrent task executions */
1093
+ class TaskPool extends TaskEventEmitter {
1094
+ constructor(name, size = 4) {
1095
+ super();
1096
+ this.id = uid();
1097
+ this.name = name;
1098
+ this.size = size;
1099
+ this._slots = Array.from({ length: size }, (_, i) => ({ id: i, busy: false }));
1100
+ this._waiting = [];
1101
+ }
1102
+
1103
+ _getFreeSlot() { return this._slots.find(s => !s.busy) ?? null; }
1104
+
1105
+ /** Submit a task to the pool; resolves when a slot becomes available */
1106
+ submit(task) {
1107
+ return new Promise((resolve, reject) => {
1108
+ const attempt = () => {
1109
+ const slot = this._getFreeSlot();
1110
+ if (!slot) { this._waiting.push(attempt); return; }
1111
+ slot.busy = true;
1112
+ this.emit('acquire', slot.id);
1113
+ Promise.resolve(task.run())
1114
+ .then(r => { slot.busy = false; this.emit('release', slot.id); this._waiting.shift()?.(); resolve(r); })
1115
+ .catch(e => { slot.busy = false; this.emit('release', slot.id); this._waiting.shift()?.(); reject(e); });
1116
+ };
1117
+ attempt();
1118
+ });
1119
+ }
1120
+
1121
+ /** Submit multiple tasks at once */
1122
+ submitAll(tasks) { return Promise.all(tasks.map(t => this.submit(t))); }
1123
+
1124
+ /** Count of currently idle slots */
1125
+ get freeSlots() { return this._slots.filter(s => !s.busy).length; }
1126
+ }
1127
+
1128
+ // ─── SequentialTask ──────────────────────────────────────────────────────────
1129
+ /** Runs raw async functions in series, piping output as input */
1130
+ class SequentialTask extends TaskEventEmitter {
1131
+ constructor(name, fns = []) {
1132
+ super();
1133
+ this.id = uid();
1134
+ this.name = name;
1135
+ this._fns = fns;
1136
+ this.stats = new TaskStats();
1137
+ this._state = State.PENDING;
1138
+ }
1139
+
1140
+ /** Append a step */
1141
+ add(fn) { this._fns.push(fn); return this; }
1142
+
1143
+ async run(initial) {
1144
+ this._state = State.RUNNING;
1145
+ const t0 = Date.now();
1146
+ let val = initial;
1147
+ for (const fn of this._fns) val = await Promise.resolve(fn(val));
1148
+ this._state = State.RESOLVED;
1149
+ this.stats.record(Date.now() - t0);
1150
+ return val;
1151
+ }
1152
+ }
1153
+
1154
+ // ─── ParallelTask ────────────────────────────────────────────────────────────
1155
+ /** Runs raw async functions concurrently and returns all results */
1156
+ class ParallelTask extends TaskEventEmitter {
1157
+ constructor(name, fns = []) {
1158
+ super();
1159
+ this.id = uid();
1160
+ this.name = name;
1161
+ this._fns = fns;
1162
+ this.stats = new TaskStats();
1163
+ }
1164
+
1165
+ /** Append a parallel step */
1166
+ add(fn) { this._fns.push(fn); return this; }
1167
+
1168
+ async run(input) {
1169
+ const t0 = Date.now();
1170
+ const results = await Promise.all(this._fns.map(fn => fn(input)));
1171
+ this.stats.record(Date.now() - t0);
1172
+ return results;
1173
+ }
1174
+ }
1175
+
1176
+ // ╔══════════════════════════════════════════════════════════════════════════╗
1177
+ // ║ SECTION 6 — Management: TaskMonitor + TaskScheduler ║
1178
+ // ╚══════════════════════════════════════════════════════════════════════════╝
1179
+
1180
+ // ─── TaskMonitor ─────────────────────────────────────────────────────────────
1181
+ /** Aggregates health and metrics across every registered task */
1182
+ class TaskMonitor extends TaskEventEmitter {
1183
+ constructor() {
1184
+ super();
1185
+ this._tasks = new Map();
1186
+ }
1187
+
1188
+ /** Register a task for monitoring */
1189
+ register(task) {
1190
+ this._tasks.set(task.id, task);
1191
+ task.on?.('complete', tr => this.emit('task:complete', task, tr));
1192
+ task.on?.('error', e => this.emit('task:error', task, e));
1193
+ return this;
1194
+ }
1195
+
1196
+ /** Deregister a task */
1197
+ unregister(task) { this._tasks.delete(task.id); return this; }
1198
+
1199
+ /** Full report for every monitored task */
1200
+ report() {
1201
+ return [...this._tasks.values()].map(t => ({
1202
+ id: t.id,
1203
+ name: t.name,
1204
+ state: t._state,
1205
+ stats: t.stats?.toJSON(),
1206
+ }));
1207
+ }
1208
+
1209
+ /** Filter tasks by state */
1210
+ byState(state) { return [...this._tasks.values()].filter(t => t._state === state); }
1211
+ /** Filter tasks by tag */
1212
+ byTag(tag) { return [...this._tasks.values()].filter(t => t.hasTag?.(tag)); }
1213
+ /** Cancel every monitored task */
1214
+ cancelAll() { this._tasks.forEach(t => t.cancel?.()); return this; }
1215
+
1216
+ /** Summary counts per state */
1217
+ health() {
1218
+ const counts = Object.fromEntries(Object.values(State).map(s => [s, 0]));
1219
+ this._tasks.forEach(t => counts[t._state]++);
1220
+ return { total: this._tasks.size, ...counts };
1221
+ }
1222
+ }
1223
+
1224
+ // ─── TaskScheduler ───────────────────────────────────────────────────────────
1225
+ /** Central registry, factory, and orchestrator for all task types */
1226
+ class TaskScheduler extends TaskEventEmitter {
1227
+ constructor(name, opts = {}) {
1228
+ super();
1229
+ this.id = uid();
1230
+ this.name = name;
1231
+ this.options = opts;
1232
+ this._tasks = new Map();
1233
+ this._mw = new TaskMiddlewarePipeline();
1234
+ this.logger = new TaskLogger({ prefix: `[${name}]`, ...opts.logger });
1235
+ this.monitor = new TaskMonitor();
1236
+ }
1237
+
1238
+ // ── Registry ──────────────────────────────────────────────────────────
1239
+ _register(task) {
1240
+ this._tasks.set(task.id, task);
1241
+ this.monitor.register(task);
1242
+ this.logger.debug(`Registered: ${task.name}`);
1243
+ this.emit('register', task);
1244
+ return task;
1245
+ }
1246
+
1247
+ /** Look up a task by name */
1248
+ get(name) { return [...this._tasks.values()].find(t => t.name === name); }
1249
+ /** Look up a task by ID */
1250
+ getById(id) { return this._tasks.get(id); }
1251
+ /** All registered tasks */
1252
+ getAll() { return [...this._tasks.values()]; }
1253
+
1254
+ /** Remove and cancel a task by ID */
1255
+ remove(id) {
1256
+ const t = this._tasks.get(id);
1257
+ t?.cancel?.();
1258
+ this._tasks.delete(id);
1259
+ this.monitor.unregister({ id });
1260
+ this.emit('remove', t);
1261
+ return this;
1262
+ }
1263
+
1264
+ /** Remove and cancel every task */
1265
+ removeAll() { [...this._tasks.keys()].forEach(id => this.remove(id)); return this; }
1266
+
1267
+ // ── Factories ─────────────────────────────────────────────────────────
1268
+ createTask(name, fn, opts)
1269
+ { return this._register(new Task(name, fn, { logger: this.logger, ...opts })); }
1270
+
1271
+ createSyncTask(name, fn, opts)
1272
+ { return this._register(new SyncTask(name, fn, opts)); }
1273
+
1274
+ createRetryTask(name, fn, opts)
1275
+ { return this._register(new RetryTask(name, fn, opts)); }
1276
+
1277
+ createTimeoutTask(name, fn, ms, opts)
1278
+ { return this._register(new TimeoutTask(name, fn, ms, opts)); }
1279
+
1280
+ createDebouncedTask(name, fn, delay, opts)
1281
+ { return this._register(new DebouncedTask(name, fn, delay, opts)); }
1282
+
1283
+ createThrottledTask(name, fn, delay, opts)
1284
+ { return this._register(new ThrottledTask(name, fn, delay, opts)); }
1285
+
1286
+ createConditionalTask(name, fn, cond, opts)
1287
+ { return this._register(new ConditionalTask(name, fn, cond, opts)); }
1288
+
1289
+ createPriorityTask(name, fn, priority, opts)
1290
+ { return this._register(new PriorityTask(name, fn, priority, opts)); }
1291
+
1292
+ createRecurringTask(name, fn, delay, unit, opts)
1293
+ { return this._register(new RecurringTask(name, fn, delay, unit, opts)); }
1294
+
1295
+ createCronTask(name, fn, expr, opts)
1296
+ { return this._register(new CronTask(name, fn, expr, opts)); }
1297
+
1298
+ createBatchTask(name, items, fn, opts)
1299
+ { return this._register(new BatchTask(name, items, fn, opts)); }
1300
+
1301
+ /** Unregistered composite helpers — don't track internally */
1302
+ createGroup(name, tasks) { return new TaskGroup(name, tasks); }
1303
+ createChain(name) { return new TaskChain(name); }
1304
+ createPipeline(name) { return new TaskPipeline(name); }
1305
+ createQueue(name, opts) { return new TaskQueue(name, opts); }
1306
+ createPool(name, size) { return new TaskPool(name, size); }
1307
+ createSequential(name, fns) { return new SequentialTask(name, fns); }
1308
+ createParallel(name, fns) { return new ParallelTask(name, fns); }
1309
+
1310
+ // ── Bulk Operations ───────────────────────────────────────────────────
1311
+ /** Run all registered tasks concurrently */
1312
+ runAll() { return Promise.allSettled(this.getAll().map(t => t.run())); }
1313
+ /** Cancel every registered task */
1314
+ cancelAll() { this.getAll().forEach(t => t.cancel?.()); return this; }
1315
+
1316
+ /** Filter registered tasks by tag */
1317
+ byTag(tag) { return this.getAll().filter(t => t.hasTag?.(tag)); }
1318
+ /** Filter registered tasks by state */
1319
+ byState(s) { return this.getAll().filter(t => t._state === s); }
1320
+
1321
+ /** Scheduler + monitor health summary */
1322
+ health() { return this.monitor.health(); }
1323
+ /** Full stats report */
1324
+ report() { return this.monitor.report(); }
1325
+ /** Register global middleware */
1326
+ use(fn) { this._mw.use(fn); return this; }
1327
+
1328
+ toJSON() {
1329
+ return {
1330
+ id: this.id,
1331
+ name: this.name,
1332
+ tasks: this.getAll().map(t => t.toJSON?.() ?? t.name),
1333
+ };
1334
+ }
1335
+ }
1336
+
1337
+ // ─── Legacy Tasker alias ─────────────────────────────────────────────────────
1338
+ /**
1339
+ * Backwards-compatible Tasker class.
1340
+ * Extends TaskScheduler with the original API surface.
1341
+ */
1342
+ class Tasker extends TaskScheduler {
1343
+ createSyncTask(name, fn, opts) { return super.createSyncTask(name, fn, opts); }
1344
+ removeTask(task) { return this.remove(task.id); }
1345
+ scheduleTask(task, delay, unit){ task.schedule(delay, unit); return this; }
1346
+ }
1347
+
1348
+ // ╔══════════════════════════════════════════════════════════════════════════╗
1349
+ // ║ Exports ║
1350
+ // ╚══════════════════════════════════════════════════════════════════════════╝
1351
+ module.exports = {
1352
+ // Constants
1353
+ units,
1354
+ State,
1355
+
1356
+ // Utility functions
1357
+ parseUnit,
1358
+ humanDuration,
1359
+ clamp,
1360
+ uid,
1361
+ deepClone,
1362
+ noop,
1363
+ isPromise,
1364
+ memoize,
1365
+ once,
1366
+ debounce,
1367
+ throttle,
1368
+ sleep,
1369
+ retryFn,
1370
+ timeoutFn,
1371
+ wrapAsync,
1372
+ formatDate,
1373
+ parseCron,
1374
+ compose,
1375
+ chunk,
1376
+ lerp,
1377
+
1378
+ // Core data classes
1379
+ TaskError,
1380
+ TaskResult,
1381
+ TaskStats,
1382
+ TaskLogger,
1383
+ TaskEventEmitter,
1384
+ TaskMiddlewarePipeline,
1385
+
1386
+ // Task classes
1387
+ Task,
1388
+ SyncTask,
1389
+ RetryTask,
1390
+ TimeoutTask,
1391
+ DebouncedTask,
1392
+ ThrottledTask,
1393
+ ConditionalTask,
1394
+ PriorityTask,
1395
+ RecurringTask,
1396
+ CronTask,
1397
+ BatchTask,
1398
+
1399
+ // Composite/structural classes
1400
+ TaskChain,
1401
+ TaskPipeline,
1402
+ TaskGroup,
1403
+ TaskQueue,
1404
+ TaskPool,
1405
+ SequentialTask,
1406
+ ParallelTask,
1407
+
1408
+ // Management
1409
+ TaskMonitor,
1410
+ TaskScheduler,
124
1411
 
125
- }
1412
+ // Legacy alias
1413
+ Tasker,
1414
+ };